Arc Forumnew | comments | leaders | submitlogin
Ruby-like in place list expansion
7 points by brett 6130 days ago | 18 comments
I'm a of ruby's in place array expansion by prefixing the array with an asterisk

  >> a = [1, 2, 3]
  => [1, 2, 3]
  >> p a
  [1, 2, 3]
  => nil
  >> p *a  # expands to: p 1, 2, 3
  1
  2
  3
  => nil
It's often really helpful with a rest parameter

  def foo(*args)
    bar(*args)
  end
In arc it would be natural to just make "@" work in unquoted context as well as quoted ones. Lists (or expressions that resolve to lists?) prefixed by @ get expanded to their elements.

Something like this:

  arc> (= l '(1 2 3))
  (1 2 3)
  arc> (pr l)
  (1 2 3)(1 2 3)
  arc> (apply pr l)
  1231
  arc> (pr @l)    ; would expand to (pr 1 2 3)
  1231
This would eliminate most if not all calls to apply and would be handy in a lot of other contexts as well.


5 points by mdemare 6130 days ago | link

Sometimes, I'm building a list, and want to include an item in the middle only if a condition is true.

    # Ruby 1.8
    insertion = new? ? ['new'] : []
    list = ['hello','brave'] + insertion + ['world']

    # Ruby 1.9
    insertion = new? ? ['new'] : []
    list = ['hello','brave',*insertion,'world']

    # Arc now
    (let insertion (if (is-new) (list "new") nil)
      (join (list "hello" "brave") insertion (list "world")))

    # With @ operation
    (let insertion (if (is-new) (list "new") nil)
      (list "hello" "brave" @insertion "world"))
I think it's worth it.

-----

3 points by shiro 6129 days ago | link

To build a list, you can already use quasiquote---a standard idiom in Lisp-family languages.

    (let insetion (if (is-new) (list "new") nil)
       `("hello" "brave" ,@insertion "world"))
What matters is to expand a given list into an argument list. To do that you have to use 'apply' now.

-----

4 points by pg 6130 days ago | link

Interesting, but does it make programs that much shorter? Can you give me an example of a piece of code that gets dramatically cleaner if you have something like you're suggesting?

-----

4 points by absz 6129 days ago | link

The biggest win, which actually results from a semantic change, is that it becomes possible to cleanly apply a macro to a variable number of arguments. Consider

  (apply and (acons x) (car x) lst)
as opposed to

  (and (acons x) (car x) @lst)
. The first version is broken: it will be forced to evaluate (acons x), (car x), and lst before applying and to them, which breaks the semantics of and: (acons x) no longer guards against (car x) trying to take the car of a non-list, as everything is evaluated before being passed to apply.

On the other hand, the second version works: the list is spliced in and then the and macro is run as normal. The semantics are preserved here because @, like a macro, expands the code first; nothing is evaluated, and then the and macro runs on its arguments as usual, short-circuiting if (acons x) fails. The variable being spliced is still evaluated, but that is the point of this notation.

As a concrete example (though I think this is generally useful/powerful), partial application then becomes merely

  (def par (fn . args)
    (fn newargs (fn @args @newargs)))
. If apply were used instead of the splicing @, this would actually break on certain macros as noted above, both causing the abstraction to leak and preventing someone from partially applying and, or, etc.

-----

2 points by bogomipz 6122 days ago | link

The more I think about this, the more mind boggling I find it. What exactly would this expression expand to?

  (and (acons x) (car x) @lst)
The actual lst is only available at runtime, while the 'and macro expands at compile time.

  Edit:
After looking at the definition of 'and, it is clear that the above could never work. Without performing the splice it would expand to

  (if (acons x) (if (car x) @lst))
For each additional argument 'and must add another 'if to the code, so with lst being spliced in at runtime, we have an impossible problem.

It seems @ can only ever work reliably in quasiquote. I guess longtime lispers already knew, otherwise we would probably have had this feature for several decades already.

If you insist, it could still be done for functions. To avoid inefficient code, it better be smart about how it builds the list;

  (foo 'a @lst)     -> (apply foo (cons 'a lst))
  (foo 'a 'b @lst)  -> (apply foo (append (list 'a 'b) lst))
  (foo @lst 'c 'd)  -> (apply foo (append lst (list 'c 'd)))
  (foo @lst 4 @bar) -> (apply foo (append lst (cons 4 bar)))
and so on.

-----

1 point by absz 6118 days ago | link

I had assumed that (and (acons x) (car x) @lst) would expand to (and (acons x) (car x) lst.0 lst.1 ... lst.N), and only then would it expand to (if (acons x) (if (car x) (if lst.0 ...))). In other words, @s are expanded as though they were the outermost macro, not the innermost. That should remove the problem of 'and having to be psychic. Your point about efficiency, however, is a very good one.

-----

2 points by bogomipz 6116 days ago | link

As mentioned, the macro expands at compile time, but the value of lst is not available until runtime.

In order to expand a call to 'and with 6 sub expressions into (if e0 (if e1 (if e2 (if e3 (if e4 e5))))), all 6 expressions must be visible at compile time.

Splicing in a list of arguments to a macro will simply never work (except by compiling the code from scratch each time you run it, i.e. limiting yourself to the most inefficient of interpreter techniques)

-----

1 point by absz 6116 days ago | link

Aha! Oh, I see. I should have realized that. Yes, that's a problem :) I'm not convinced it's insurmountable, but this syntax certainly won't work. Thank you.

I still, however, think it might be nice for functions, but that's merely a difference in appearance, not functionality.

-----

4 points by brett 6130 days ago | link

Off the top of my head it's the 5 characters you save on calls to apply. Also, as opposed to apply, you keep the function in functional position which seems cleaner to me.

I'll keep thinking about common cases where it's a bigger win. A more elaborate call to apply where you have to compose the list comes to mind:

  (apply func (join lst '(a)))
vs

  (func @lst 'a)

-----

1 point by pg 6128 days ago | link

(apply func (+ lst '(a)))

-----

1 point by lojic 6127 days ago | link

Yours is 71% longer. That does seem significant. Or the auto-expand is 42% shorter. Wow, what are the chances that it'd be 42% shorter when I just watched Hitchhiker's Guide on TiVo the other night?

-----

1 point by nex3 6127 days ago | link

Perhaps... one in forty-two?

-----

2 points by eds 6130 days ago | link

Would it not be possible to use the "." syntax that already exists? For example, if + were defined something like this:

  (def + args
    ...)
Then to call + on a list a which contains '(1 2 3):

  (+ . a)
I know this doesn't work in the current Arc, but maybe support for it could be added. (Or maybe not if there is some other reason you can't do that with lists... but I can't think of any such reason.) At any rate you shouldn't have to define new syntax for it to make it work, you just have to make the interpreter eval the form (fn . args) correctly. (In CL it works if you try "(eval `(+ . ,a))", which isn't exactly the same thing, but gets at what I am proposing.)

-----

4 points by vsingh 6130 days ago | link

Your solution is not sufficiently general.

For example, it should keep working if the 'a' in (+ . a) is replaced with its actual value. In that case, we get:

    (+ . '(1 2 3))
which is the same thing as saying

    (+ . (quote (1 2 3))
which is equivalent to

    (+ quote (1 2 3))
which obviously doesn't do the same thing as

    (apply + '(1 2 3))
which is what you wanted.

-----

1 point by bogomipz 6122 days ago | link

That means (+ . (1 2 3)) does what you want, doesn't it?

Granted, to do the equivalent of (+ @(foo)) you would have to do;

  (let temp (foo)
       (+ . temp))
Which is not very nice at all, so I'm not arguing against @. On the contrary.

Edit: I used to think that . and @ would only differ in the sense that cons and splice have different list building semantics, but now I believe that they should also differ in the timing of the operation;

  (+ . (foo))  -> (+ foo)
  (+ . '(foo)) -> (+ quote foo)
  (+ @(foo))   -> what we want, given that foo returns a list
  (+ @'(foo))  -> (+ 'foo)

-----

2 points by shiro 6130 days ago | link

That's a tempting idea; the problem is that you can only use a single variable after the dot. Because of the very definition of S-expression,

    (+ . (f x y))
is indistinguishable from

    (+ f x y)
However, just allowing single-variable case may be good to cover typical cases.

-----

3 points by absz 6130 days ago | link

It's close, but doesn't quite have the same semantics: with your syntax, the variable has to be at the end, but using the @ for splicing, one can write (+ 1 2 @lst 3 4).

-----

2 points by bogomipz 6129 days ago | link

Yes, which also means the @ cannot reuse the lst object in this case, and therefore shouldn't do so when it happens to be at the end either. The dot is a notation for CONS, while @ would require copying the list that is spliced in.

Both . and @ are very useful and would be great to have available in any context.

-----