-
-
Notifications
You must be signed in to change notification settings - Fork 46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Seqs & iterators #22
Comments
I'm not sure yet how this relates to things like |
It looks like destructuring uses the iterable protocol, so we can easily do export function first(coll) {
// destructuring uses iterable protocol
let [first] = coll;
return first;
}
export function rest(coll) {
let [_, ...rest] = coll;
return rest;
} and get the behavior we want, fast. |
A downside I've found in implementing & using this in tests is that objects (as created via You can get access to an iterator via functions like A thought I have: provide a helper function Thoughts? |
An internal function is always ok. Not sure if we should expose this. Perhaps we could expose it as |
There's also a |
Writing down some notes for posterity:
The thing that makes this tricky is that the Perhaps |
Hmmm but |
Unless those explicitly support |
@borkdude In the PR I just opened, I am trying the following:
|
@lilactown So far this makes a ton of sense to me :) Perhaps add a bit to the README about differences to ClojureScript, basically what you said in your last comment |
Alright, I think we could close this now, probably, since we pretty much locked down the seq + iterator behavior, unless you have something to add @lilactown ? |
I think the only thing left is to add the rest of the sequence functions, which I can create more issues for |
@lilactown function _first([x]) {
return x;
}
function _toArray(iterable) {
const array = [];
for (const x of iterable) {
array.push(x);
}
return array;
}
function* _map(iterable, f) {
for (const x of iterable) {
yield f(x);
}
}
function* _filter(iterable, pred) {
for (const x of iterable) {
if (pred(x)) {
yield x;
}
}
}
function map(f, coll) {
return _toArray(_map(coll, f));
}
function filter(pred, coll) {
return _toArray(_filter(coll, pred))
}
console.log(map((v) => v + 1,new Array(1, 2)))
console.log(filter((v) => v === 2, new Array(1, 2)))
function transformer(iterable) {
return {
first: () => _first(iterable),
map: f => transformer(_map(iterable, f)),
filter: pred => transformer(_filter(iterable, pred)),
toArray: () => _toArray(iterable),
[Symbol.iterator]: () => iterable[Symbol.iterator](),
}
}
console.log(
transformer(new Array(1, 2))
.map(v => v + 1)
.filter(v => v == 2)
.first()
);
console.log(
transformer(new Array(1, 2))
.map(v => v + 1)
.filter(v => v == 2)
.toArray()
); |
Background
The Clojure "seq" is a key concept in its language design. It provides a common abstraction for working with all kinds of collections where you want to treat them as an ordered sequence of elements.
JS has a similar abstraction called Iterators. Like a collection in Clojure can be "seqable," a collection in JS can be "iterable." Like seqs, iterators can lazily generate each element or be a view on top of an eager collection. The Iterator & Iterable protocols in JS underpin fundamental operations like
for...of
, similar to how Clojure seqs underpinmap
,filter
et al. There also exist ways to create custom Iterators in JS using generators, just like in Clojure you can construct lazy sequences vialazy-seq
.Currently, iterators do not have any operations on them except the commonly use
for ... of
comprehension. There is currently a stage 2 proposal for adding more helpers like.map
,.filter
, etc: https://github.com/tc39/proposal-iterator-helpersThe major difference between the two abstractions is that iterators are mutable. Consuming an element in the iteration by calling the
.next()
method mutates the iterator object in place. This makes passing a reference of an iterator to a function a somewhat dangerous thing, as you can't be sure whether or not it consumes it.Proposal
I would propose two things. I am still thinking this through, so feedback welcome:
map
,filter
, etc. should accept anything that isIterable
and return a concrete array typeRationale
Seqs work well in Clojure because collections are immutable, so an immutable view on top of them does not need to worry about the underlying collection changing. In JS we do not have this guarantee, and an immutable view on top of a mutable collection can lead to surprising behavior. Clava also has a general philosophy of "use the platform" rather than bringing over Clojure abstractions. All of this is why I think basing our sequential operators on Iterators makes more sense than porting seqs.
In the above section, I proposed that we take anything that is Iterable but always return an array in
map
,filter
, etc. The alternative would be to return an Iterator directly, similar to the TC39 proposal I linked in the background. However, I think this would lead to a lot of confusion when writing application code. Take the following example:If
map-indexed
returns an Iterator, this code is broken: the(count todos-with-id)
will consume the iterator, which will lead to nothing being in the Iterator when React tries to render the divs. Developers will have to be careful in their application code to only use the result of amap
,map-indexed
orfilter
once. I think that this is an unreasonable thing to force developers to keep in mind, which is why I think that we should do these operations eagerly and return an array.For transducers, however, we can own the Iterator as long as it's in the transducer pipeline. So code like:
(range 10)
could return an Iterable (e.g. an array). The transducer pipeline can pass an iterator to each stage, avoiding the cost of creating an array each time, before finally adding it all to the array passed to into.Gotta stop here. Will add more thoughts later. Questions and comments welcome!!
The text was updated successfully, but these errors were encountered: