coco
coco copied to clipboard
Array spread operator
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.
+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])
Requiring spacing is imho something needed anyway.
With partially-applied operators from LiveScript and Prelude.ls, this becomes a little gratuitous. map (.property) array would be sufficiently descriptive and terse.
You forgot a ,. And with some operations this could slow down thingd
@Nami-Doc: Whoops, I was thinking about Haskell.
map (.property) arraywould 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
Another thought: providing destructuring inside a coco's current support for destructuring assignment in a for loop does cover some of the use cases, albeit more verbosely :for statement is another way to provide some of the functionality, 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
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.
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.
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
array * -> parseInt it, 10=>[].map.call array, -> parseInt it, 10
Like it. Would have done this already if we were assuming ES5.
map$ = [].map || function... is there something not replicable?
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.
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-ofrelying 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 * -> sugarwould 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.
I love both syntaxes. One without side effects (*), the other with (for &)
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.
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.
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).
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.
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.
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$));
}
}