ftor ftor - 2 months ago 16
Javascript Question

How to map over arbitrary Iterables?

I wrote a

reduce
function for
Iterable
s and now I want to derive a generic
map
that can map over arbitrary
Iterable
s. However, I have encountered an issue: Since
Iterable
s abstract the data source,
map
couldn't determine the type of it (e.g.
Array
,
String
,
Map
etc.). I need this type to invoke the corresponding identity element/concat function. Three solutions come to mind:


  1. pass the identity element/concat function explicitly
    const map = f => id => concat => xs
    (this is verbose and would leak internal API though)

  2. only map
    Iterable
    s that implement the monoid interface (that were cool, but introducing new types?)

  3. rely on the prototype or constructor identity of
    ArrayIterator
    ,
    StringIterator
    , etc.



I tried the latter but
isPrototypeOf
/
instanceof
always yield
false
no matter what a do, for instance:

Array.prototype.values.prototype.isPrototypeOf([].values()); // false
Array.prototype.isPrototypeOf([].values()); // false


My questions:


  • Where are the prototypes of
    ArrayIterator
    /
    StringIterator
    /...?

  • Is there a better approach that solves the given issue?



Edit:
[][Symbol.iterator]()
and
("")[Symbol.iterator]()
seem to share the same prototype:

Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]())) ====
Object.getPrototypeOf(Object.getPrototypeOf(("")[Symbol.iterator]()))


A distinction by prototypes seems not to be possible.

Edit: Here is my code:



const values = o => keys(o).values();
const next = iter => iter.next();

const foldl = f => acc => iter => {
let loop = (acc, {value, done}) => done
? acc
: loop(f(acc) (value), next(iter));

return loop(acc, next(iter));
}


// static `map` version only for `Array`s - not what I desire

const map = f => foldl(acc => x => [...acc, f(x)]) ([]);


console.log( map(x => x + x) ([1,2,3].values()) ); // A

console.log( map(x => x + x) (("abc")[Symbol.iterator]()) ); // B





The code in line
A
yields the desired result. However
B
yields an
Array
instead of
String
and the concatenation only works, because
String
s and
Number
s are coincidentally equivalent in this regard.

Edit: There seems to be confusion for what reason I do this: I want to use the iterable/iterator protocol to abstract iteration details away, so that my fold/unfold and derived map/filter etc. functions are generic. The problem is, that you can't do this without also having a protocol for identity/concat. And my little "hack" to rely on prototype identity didn't work out.

@redneb made a good point in his response and I agree with him that not every iterable is also a "mappable". However, keeping that in mind I still think it is meaningful - at least in Javascript - to utilize the protocol in this way, until maybe in future versions there is a mappable or collection protocol for such usage.

Answer

I have not used the iterable protocol before, but it seems to me that it is essentially an interface designed to let you iterate over container objects using a for loop. The problem is that you are trying to use that interface for something that it was not designed for. For that you would need a separate interface. It is conceivable that an object might be "iterable" but not "mappable". For example, imagine that in an application we are working with binary trees and we implement the iterable interface for them by traversing them say in BFS order, just because that order makes sense for this particular application. How would a generic map work for this particular iterable? It would need to return a tree of the "same shape", but this particular iterable implementation does not provide enough information to reconstruct the tree.

So the solution to this is to define a new interface (call it Mappable, Functor, or whatever you like) but it has to be a distinct interface. Then, you can implement that interface for types that makes sense, such as arrays.

Comments