Next step (based on the incubator call of Aug 10 2020)
Jack-Works opened this issue · comments
Reference: https://github.com/tc39/incubator-agendas/blob/master/notes/2020/08-10.md
- Semantic decision: Iterable class instead of the current design. The class design also makes it extensible in the future.
- Full-featured or minimal design: Add useful methods if possible. Learn from other languages.
- API ergonomics:
- Class is not easy to use (they need a
new
op to create).- Possible solution 1: Make it a callable class (like
Array
vsnew Array
) - Possible solution 2: A hidden class that not exposed to the global scope (`Range`), only expose the "class builder" as a plain function to
new
the class in the backend.
- Possible solution 1: Make it a callable class (like
- I've heard some requirement that if it is a class, the name must be capitalized (`Range` instead of `range`). I didn't like the idea but if we go through 3.i.b this is no longer a problem.
- Class is not easy to use (they need a
- More abstraction on Range?: The idea of a "general" protocol of range that allows future consistent extension of Array Range, String Range, ...etc. Developers can even "implement"/"extends" to make their own ranges. I'm not confident with this idea and doubt if this is really useful or works in JavaScript.
- I have no intention to work on this idea currently.
- But an important problem to consider: If we have a String.range (or whatever) in the future, should it share a bass class with Number.range and BigInt.range?
- Maybe use Cognitive Dimensions of Notation to investigate designs
I have no enough free time to work on this these days so sorry for the delay (it's nearly a whole month since the incubator call).
Generally, my next step is to migrate the current design with no observable API style changes (e.g. from Number.range
to new Number.range
(and I don't like that)). That means I will choose one of 3.i.a (callable class) or 3.i.b (hidden class, exposed create helper). Please leave comments on which side you'd like to see.
I remain strongly against the class approach, and the notes don't make it clear to me what the persuasive argument was (nor what future extensibility you have in mind, or why that extensibility can't be achieved with the function approach). Per the "base class" question you raise, it feels to me like extensibility is harder with a class hierarchy due to the fragile base class problem.
New classes shouldn't be callable, since class
can't be callable. I agree that if it's a constructor, its name should follow the almost universal convention of being in PascalCase (ie, Range, not range).
I'd be very interested to see the results of an informed investigation into various designs.
nor what future extensibility you have in mind, or why that extensibility can't be achieved with the function approach
To extends, means helper methods. Yes, they can be added in the current design but as you can see, it is adding methods on the RangeIteratorPrototype
which makes the semantics of helper methods ambiguous. (If we have a .contains()
, should it consume the iterator since it is on the iterator prototype?). Another thing to mention: the current design is adding internal slots on a built-in generator.
New classes shouldn't be callable, since class can't be callable.
If so, I'll choose the 3.i.b (hidden class, exposed create helper) route to make the change (at least for now) so I can bypass the PascalCase requirement (also, at least for now, I'd be able to change if it is found harmful or not welcomed design).
A RangeIteratorPrototype
is possible with either design, so I'm not clear on why extensibility there has any effect on class vs factory.
When you say "hidden class", if the constructor is reachable, it'd need to have a PascalCase name. If it's not reachable, then it seems like it's just the factory approach, where range
is a function that produces an iterator (that inherits from RangeIteratorPrototype)?
A RangeIteratorPrototype is possible with either design
Yes, I have mentioned that "which makes the semantics of helper methods ambiguous. (If we have a .contains(), should it consume the iterator since it is on the iterator prototype?)."
When you say "hidden class", if the constructor is reachable, it'd need to have a PascalCase name. If it's not reachable, then it seems like it's just the factory approach
It's not the case. I'm meaning (the second approach):
const r = Number.range(0, 1)
const hiddenRangeCtor = r.constructor
assert(hiddenRangeCtor.name === 'NumberRange') // or other name
assert(r instanceof hiddenRangeCtor)
assert(!(r instanceof Number.range))
Ah, I see what you mean. In either case, yes, I would assume a generic .contains()
consumes the iterator, but that a Range-specific contains
need not.
Thanks, your code sample matches my expectation if we were to go with the class instance approach. Presumably in that case we'd make hiddenRangeCtor
also non-constructible (or not accessible via .constructor
), so you couldn't do new hiddenRangeCtor
?
Presumably in that case we'd make
hiddenRangeCtor
also non-constructible (or not accessible via.constructor
), so you couldn't donew hiddenRangeCtor
?
I think it is no harm to get the underlying Range class and construct it manually. My expectation is that: Number.range = (...args) => new #hiddenNumberRangeCtor#(...args)
to improve ergonomics of the API.
@codehag hi! Could you give help to use Cognitive Dimensions of Notation to investigate which semantics is better? Current semantics VS #42. It seems like both Iterator and Iterables have their supporters and it's hard to going on now.
cc @Felienne
This looks interesting. I want to think about it a bit, but wanted to make sure Felienne also saw.
In the MDN Iterator example the authors aptly named the function makeRangeIterator
as to highlight that the result of that operation would be an iterator and not a data structure (Iterable).
From my experience with other libraries: Number.range(0, 100, 2)
suggests to return a data structure (lodash). To suggest an Iterator I would be looking at something like Iterator.numberRange(0, 100, 2)
or shorter Iter.range(0, 100, 2)
.
While I think that having an Iterator is a perfectly fine simple step, I think there can be an argument made for a data structure: doing an .includes
when having a data structure allows for a O(1)
complexity (sample implementation) while doing an .includes
on an Iterable would be a O(N)
complexity.
Adding both Iterator.range
and Number.range
seems not necessary. I'm wondering if we choose the name as Iterator.range
, does it release the harm in of not-reusbility?
Iterator.range
kind of kills the ability to support multiple number types - ie, BigInt, or a future Decimal.
One would need to get creative about naming: Iterator.bigIntRange
but there might be another name that is better suited?
Aside from this, I still think that there are enough arguments for having an Iterable over a Iterator.
@martinheidegger i don't agree with that last point; i still haven't heard any arguments that hold. There's lots of discussion about that on #42, though, so let's have that there.