-
Notifications
You must be signed in to change notification settings - Fork 34
Consider making flatMap throw if the mapper returns a non-iterable #55
Comments
Does this mean that a flatMap callback would be unable to return a non-iterable? |
Doing so would cause |
That seems highly unusable - it means i always have to return an iterable, which effectively means everyone will slap |
If you are using |
I use flatMap when I want to not care about whether I'm returning an iterable or not. |
@ljharb How do you want strings returned by your mapping function handled? |
Would this not be true regardless with flatMap? (i.e. |
Generally speaking we regard turning an error case into a non-error case as being not breaking, so I don't think so.
That would never attempt to treat |
Doesn’t flatMap take a depth argument? I assume I’d use that to determine if i wanted my string iterator consumed or not (in which case yes, I’d wrap strings in an array, but forcing that for every other primitive seems harsh) |
No. |
ah right, that’s only .flat - forget about that part :-) so then yes, it’s that for strings, because they’re iterable, I’d have to wrap them in another iterable - but forcing that on all the other primitives (and non iterable objects) seems like a high price to pay. |
I think the main use case for
Given that relying this fallback mechanism requires remembering which things it's going to iterate and which it is going to leave alone, I really don't think the benefit is there. Like it says in the OP, anyone who's gotten accustomed to relying on it for the other primitive types is going to be really confused by the behavior for strings. Better that they just use the consistent interface. |
in the interest of covering all the bases, we could also special case strings |
I would be very strongly opposed to special casing strings. |
This is how this works in Java 8’s streams, where the This is also why Java has the static |
I'm not a fan of this. I'm for better consistency with |
In the future, to iterators prototype could be added |
Note that it was explicitly decided against flattening iterables because they weren't Arrays. Similarly tterables aren't iterators either so in my opinion the array thing isn't a strong argument. Instead it could be an option to just flatten other Iterator.from([1,2,3,4])
.flatMap(i => i)
.toArray(); // [1,2,3,4]
Iterator.from([1,2,3,4])
.flatMap(i => [i, i]);
.toArray(); // [[1,1], [2,2], [3,3], [4,4]]
Iterator.from([1,2,3,4])
.flatMap(i => Iterator.from([1,1])) // Iterator flattening
.toArray(); // [1,1,2,2,3,3,4,4] |
@Jamesernator for better, but not for full -) The maximum consistency of methods of different classes is a good practice for languages and minimizes language learning problems. Some difference between methods makes sense, like usage iterables in Some - just makes more complexity and make language learning more difficult, like removing optional |
You might come from a different mindset, but that's really not how programming should work. Relying on dynamic types makes code hard to maintain and implementations hard to optimise. We suffered from this problem with the "promise or thenable or maybe not and just a plain value instead" return value of the promise One should use If one does not care and wants the code to work with both, one should explicitly call a
I'm not sure where you'd need that - most of those cases should probably be using function filterValuesByKey(iterator, predicate) {
return iterator.flatMap(([key, value]) => predicate(key) ? value : [])
// ^^^^^^^ assuming the overloaded version
}
new Set(filterValuesByKey(map.entries(), …)) then I would argue that |
This is very motivating to me. However, we do still have Array#flatMap which behaves differently. |
@devsnek Tbh I have exactly the same concerns about |
function filterValuesByKey(iterator, predicate) {
return iterator.flatMap(([key, value]) => predicate(key) ? value : [])
} If you don’t want function filterValuesByKey(iterator, predicate) {
return iterator.flatMap(([key, value]) => predicate(key) ? [value] : [])
} You’ll want the following, which is more performant, because it doesn’t allocate extra arrays, which is why this proposal is being made in the first place: function filterValuesByKey(iterator, predicate) {
return iterator.filter(([key /*, value*/]) => predicate(key))
} |
@ExE-Boss Yes, one could write @zloirock As I said, it's more about correctness. |
@bakkot you can write a rule which enforces |
@zloirock It would need a typechecker, not a linter, to recognize the need. |
@bergus see above. |
@zloirock The web platform will generally try to avoid shipping breaking changes, yes. Which means that if we add this feature, and people start using it, they will never be able to add |
@bakkot your example too far-fetched, so don't worry about it -) |
@zloirock As another example, consider recursively |
@bergus ...and? In cases like that, on objects, sure, better wrap the result to something iterable, like an array, but why should I wrap to array primitives? Why should I write a callback several times longer and sacrifice performance? Why should I remember one more difference between this and |
On |
I'd still like to point out the option to flatten other Iterators only: e.g. function* flatMap(mapperFn) {
for (const value of this) {
const mapped = mapperFn(value);
if (hasSlot(mapped, [[Iterated]])) {
// flatten mapped
} else {
yield mapped;
}
}
} instead offunction* flatMap(mapperFn) {
for (const value of this) {
const mapped = mapperFn(value);
if (mapped[Symbol.iterator]) {
// flatten mapped
} else {
yield mapped;
}
}
} This gives fallback behavior for non-iterators, avoids the no-upgrade to |
Assuming you mean iterator records ( |
Looks like I was assuming Edited pseudocode: function* flatMap(mapperFn) {
for (const value of this) {
const mapped = mapperFn(value);
if (mappped instanceof Iterator) { // NOT checking for Iterable
// flatten mapped
} else {
yield mapped;
}
}
} |
Being an instance of Iterator is not a requirement of the iterator protocol, so we shouldn't use that as a check. |
@Jamesernator I think it has been mostly agreed about in the discussion #47 (comment) that flattening iterables is much more useful than flattening iterators. But regardless which protocol we check for (by testing the existence of a |
I think it should throw an exception, line any sane programming language would. |
Let’s avoid terms like “sane”, please. |
The same was argued for And if we agree operating on iterables is strictly more useful I'm still not sure why this proposal isn't for operating on iterables instead.
I think we should avoid discussing specific features and instead focus on the problems. The reason I came up with |
@Jamesernator If we did iterator-flattening instead of iterable-flattening, the issue would be an upgrade-to-iterator hazard instead. (Which probably might happen even more accidentally, as people don't realise a |
My background: I maintain iter-tools. My thoughts: I definitely think If I'm being really honest, I don't even think iterator should be a separate protocol. It's just needlessly confusing. It is in essence a subpart of the iterable protocol, which interestingly does not specify whether separate iterators returned from
|
Note I am specifically suggesting only flattening those that inherit |
In
Array.prototype.flatMap
, if the mapping function returns a non-array, it is treated as if it returned an array of length 1. That was to preserve symmetry with.flat
.Here, I think it makes sense to be more strict (that is, to throw), for three reasons:
.flat
to keep parity with..flatMap
flattens all iterables, not just arrays. In particular, it flattens strings. I think anyone relying on the auto-boxing behavior would find that surprising, so I think we should just not have the auto-boxing behavior.Symbol.iterator
method to any object which did not previous have it would be a breaking change.The text was updated successfully, but these errors were encountered: