satyr / coco

Unfancy CoffeeScript
http://satyr.github.com/coco/
MIT License
498 stars 48 forks source link

Array spread operator #187

Open qqueue opened 11 years ago

qqueue commented 11 years ago

Similar to Groovy's spread operator:

array*.property

as sugar for:

element.property for element of array

Essentially, the spread operator is a syntax level map or forEach for simple cases, without the performance hit of the ES5 versions and more compact syntax. Also somewhat an extension of unary spread (#98).

Use cases

document.querySelectorAll 'a[href$=png]' *.addEventListener \click !->
  console.log "opened an image!"

max-height = Math.max.apply Math, document.images*.height

Sub-decisions

Precedence

Should:

expr*.sub.property.access

compile to:

el.sub.property.access for el of expr

or:

(el.sub for el of expr).property.access

Guards

The groovy spread desugars as:

parent*.action                             //equivalent to:
parent.collect{ child -> child?.action }

Note the guarded property access. I don't think the coco version needs the guard by default. Perhaps instead we could provide a separate expr*?prop syntax (distinct from expr?*prop).

ADI

ADI for the spread operator would clobber compact multiplication a*b, though it's not a huge deal since dash-identifiers does the same to subtraction.

goto-bus-stop commented 11 years ago

+1!

I'd expect foo*.a.b.c to compile to a.b.c for a of foo, one could add parens to change that behavior, whereas that is not possible if it's the other way around (as foo*.(a.b.c) is already foo*[a.b.c])

vendethiel commented 11 years ago

Requiring spacing is imho something needed anyway.

michaelficarra commented 11 years ago

With partially-applied operators from LiveScript and Prelude.ls, this becomes a little gratuitous. map (.property) array would be sufficiently descriptive and terse.

vendethiel commented 11 years ago

You forgot a ,. And with some operations this could slow down thingd

michaelficarra commented 11 years ago

@Nami-Doc: Whoops, I was thinking about Haskell.

qqueue commented 11 years ago

map (.property) array would be sufficiently descriptive and terse.

However, this would run -> it.property for each element, which is slower than a for-loop (though a sufficiently-smart compiler would help here, of course). Also, partial application of . requires parenthesis, which we aren't too keen on around here.

Other thoughts:

I can imagine extending this proposal to pipes as well, since they are essentially applicable expressions:

sum = 0
numbers *|> sum += &
# =>
sum += xs$ for xs$ of numbers

Or even more crazy overloads of *:

sum = 0
sum +=* numbers

# expr * <function literal> 
array * -> parseInt it, 10
# =>
[].map.call array, -> parseInt it, 10
qqueue commented 11 years ago

Another thought: providing destructuring inside a for statement is another way to provide some of the functionality, albeit more verbosely coco's current support for destructuring assignment in a for loop does cover some of the use cases, albeit more verbosely :

heights = [height for {height} of document.images]

coords = [(x,y,z) for {x,y,z} of points]
# =>
coords = []
for point of points
  x = point.x, y = point.y, z =point.z
  coords.push x, y, z
vendethiel commented 11 years ago

Destructuring is indeed nice there, and unrelated to the issue (tbh I thought coco allowed it since LS does, suprised here) map returns but in prelude you also have each.

qqueue commented 11 years ago

tbh I thought coco allowed it since LS does

Whoops, coco does indeed support destructuring forms in the for loop, didn't test it beforehand.

satyr commented 11 years ago

Two concerns OTTOMH:

Evaluation

While the simple unrolling to for is appealing, it's also misleading that the subsequent chains are evaluated multiple times (or not at all). This becomes significant when your spread call takes arguments with costly operations and/or side-effects.

For comparison, Groovy's spread evaluates the arguments first, then passes the values around:

$ groovy -e 'print([0, 1]*.plus({println 2; 3}()))'
2
[3, 4]

Narrowness

The proposed syntax limits its application to property access and method calling. This is much less useful in JS (especially ES3 where extensions of native prototypes are frowned upon) than Groovy.

I'd prefer something more generic, say:

for array => &property  # for x of array => x.property

as an analogue to:

with array => &property
satyr commented 11 years ago

array * -> parseInt it, 10 => [].map.call array, -> parseInt it, 10

Like it. Would have done this already if we were assuming ES5.

vendethiel commented 11 years ago

map$ = [].map || function... is there something not replicable?

satyr commented 11 years ago

Shim-able, but we wouldn't since for+let does better.

We could do without for-of relying entirely on native Array extras if ES5 was the starting point. array * -> sugar would be viable then, compiling to [].map.call or [].forEach.call accordingly.

qqueue commented 11 years ago

Evaluation

I actually ran into this problem where I discovered this use case:

for el of $$ \.file
  el.addEventListener \click construct-click-fn \type

Where construct-click-fn \type was being run for each event in the loop, unnecessarily.

To solve this, the spread operator could cache any complex expressions in the RHS:

var len, ref$, i, ref$x
for (i = 0, ref$ = $$('.file'), len = ref$.length, ref$x = constructClickFn('type'); i < len; ++i) {
   ref$[i].addEventListener('click', ref$x)
}

Similar to other sugars in coco.


Narrowness

True that the spread star is pretty limited, but it's hard to beat the conciseness as opposed to for array => &property or prop for {prop} of array. It also allows essentially compile-time jQuery syntax, like my examples or:

$$ 'a[href^=http]' *.classList.add \external
# like
$ 'a[href^=http]' .addClass \external
# but faster

At the very least, it's no more narrow in application than the repeat * or split / sugar.

For the more general case:

for array => &property # for x of array => x.property

This syntax would be good, or perhaps a "spread pipe":

array *> do &stuff with &element

Though you could also cache complex expressions in this syntax, it's probably harder to avoid caching wanted side effects, e.g.

number *> sum += &

We could do without for-of relying entirely on native Array extras if ES5 was the starting point.

But the Array extras are still quite a bit slower than the for loop, which I think is a bigger argument for keeping it.

array * -> sugar would be viable then,

Couldn't we still compile that sugar to for loops?

stuff = array * -> it.sugar
# =>
fn = -> it.sugar
stuff = => fn el for el of array

I used [].map in my example because it was shorter, but it doesn't have to be used or shimmed for the syntax to work.

vendethiel commented 11 years ago

I love both syntaxes. One without side effects (*), the other with (for &)

satyr commented 11 years ago

the spread operator could cache any complex expressions in the RHS:

The RHS can be arbitrarily complex. Caching is nonviable unless we limit it to a single access/call, narrowing it further.

But the Array extras are still quite a bit slower than the for loop

I was assuming an idealistic ES5 where those methods and closures were so well-optimized that we wouldn't have to avoid them.

qqueue commented 11 years ago

The RHS can be arbitrarily complex. Caching is nonviable unless we limit it to a single access/call, narrowing it further.

Yeah, but the spread operator is such a simple sugar that the RHS is very likely not complex, so as long as we choose a working compilation strategy (cache all expressions in the RHS, perhaps), It's unlikely anybody will notice.

It's kind of like destructuring; you can do really complicated destructures like {{foo: [a, b{other}]}: thing} = stuff and it will work, but it would be much clearer to just write it out. Likewise, I doubt the spread operator in groovy gets much use with side effects because it's hard to understand for the reader and writer. Yet in the simple case the shorter syntax is clearer because of its brevity.

And in regards to narrowing it further, such a neutered spread operator would still be at least as useful as, say, the prototypical clone unary operator ^.

I'll draw the parallel to jQuery again: all its methods are spread by default. Having syntax-level support for spreads essentially enables to use jQuery without including jQuery, while also freeing ourselves from predefined spread methods that operate only on DOM elements (or writing boilerplate for custom methods). Similarly, cascades are just syntax-level method chaining. There's clearly a demand for these sugars, even as slower function calls, so if we can get them faster, without writing any extra code, that's a win.

satyr commented 11 years ago

the spread operator is such a simple sugar that the RHS is very likely not complex

Your example with multi-level access (expr*.sub.property.access) is already complex, yet has no clear way to be cached (unlike, say, a.b.c ||= d) since the accesses happen after the spread.

To rephrase, there would be no way to cache if the spread were to consume the rest of the chain. It'd have to be either Groovy-style (single access/call with arguments caching) or soak-style (simple unrolling).

qqueue commented 11 years ago

But unlike a.b.c ||= d, property access is intrinsically uncachable in the spread. Everything except the . chain could still be cached:

expr*.sub.property.access
#=>
el.sub.property.access for el of expr

expr*.dyn[if foo then \bar else \baz]property
#=>
ref$ = (if foo then \bar else \baz)
el.dyn[ref$]property for el of expr

expr*.safe?access.but-with complicated! arguments !-> ...
#=>
ref$ = complicated! arguments !-> ...
el.safe?access.but-with ref$ for el of expr

Sure, this can break if some of the properties are actually getters with side effects, or some of the method calls have side-effects that would have affected the cached value of an expression, but that shouldn't stand in the way of a compilation strategy that will work in the majority of cases.

Furthermore, if the caching style of *. doesn't work for a use case, regular for-loops aren't going away, or it could instead use the proposed

for array
  &whatever-you-want-here

For a simpler but still syntactically shorter compilation.

satyr commented 11 years ago
ref$ = complicated! arguments !-> ...
el.safe?access.but-with ref$ for el of expr

That way you mess up the evaluation order, which I think is worse than leaving them uncached.

vendethiel commented 11 years ago
var ref$, ref1$, i$, ref$, len$, el, ref2$;
for (i$ = 0, len$ = (ref$ = expr).length; i$ < len$; ++i$) {
  el = ref$[i$];
  if ((ref1$ = el.safe) != null) {
    ref1$.access.butWith((ref$ == null && $ref = complicated()(arguments, function(){
  throw Error('unimplemented');
}), ref$));
  }
}