I'm trying to add Flow type information to a small library of mine.
The library defines some functions that are generic over Object, Array, Set, Map and other types.
Here a small piece example to give an idea:
function set( obj, key, value ) {
if( isMap(obj) ) { obj.set(key, value); }
else if( isSet(obj) ) { obj.add(value); }
else { obj[key] = value; }
}
function instantiateSameType( obj ) {
if( isArray(obj) ) { return []; }
else if( isMap(obj) ) { return new Map(); }
else if( isSet(obj) ) { return new Set(); }
else { return {}; }
}
function forEach( obj, fn ) {
if( obj.forEach ) obj.forEach( ( value, key )=>fn(value, key, obj) );
else Object.entries(obj).forEach( ([key, value])=>fn(value, key, obj) );
}
function map( obj, fn ) {
const result = instantiateSameType( obj );
forEach(obj, (value, key)=>{
set( result, key, fn(value, key, this) );
});
return result;
}
How can I define types for map?
I'd want to avoid giving a specialized version for each of the 4 types I listed in the example, as map is generic over them.
I feel the need to define higher-order interfaces, and implement them for existing types, but can't find much about any of this...
Any hints or ideas?
Update 2017-11-28: fp-ts is the successor to flow-static-land. fp-ts is a newer library by the same author. It supports both Flow and Typescript.
There is a library, flow-static-land, that does something quite similar to what you are attempting. You could probably learn some interesting things by looking at that code and reading the accompanying blog posts by #gcanti. I'll expand on the strategy in flow-static-land; but keep in mind that you can implement your iteration functions without higher-kinded types if you are OK with a closed set of iterable types.
As #ftor mentions, if you want polymorphic functions that can work on an open set of collection types then you want higher-kinded types (HKTs). Higher-kinded types are types that take type parameters, but with one or more of those parameters left unspecified. For example arrays in Flow take a type parameter to specify the type of elements in the array (Array<V>), and the same goes for maps (Map<K, V>). Sometimes you want to be able to refer to a parameterized type without specifying all of its type parameters. For example map should be able to operate on all arrays or maps regardless of their type parameters:
function map<K, A, B, M: Array<_> | Map<K, _>>(M<A>, fn: A => B): M<B>
In this case M is a variable representing a higher-kinded type. We can pass M around as a first-class type, and fill in its type parameter with different types at different times. Flow does not natively support HKTs, so the syntax above does not work. But it is possible to fake HKTs with some type alias indirection, which is what flow-static-land does. There are details in the blog post, Higher kinded types with Flow.
To get a fully-polymorphic version of map, flow-static-land emulates Haskell type classes (which rely on HKTs). map is the defining feature of a type class called Functor; flow-static-land has this definition for Functor (from Functor.js):
export interface Functor<F> {
map<A, B>(f: (a: A) => B, fa: HKT<F, A>): HKT<F, B>
}
The HKT type is flow-static-land's workaround for implementing higher-kinded types. The actual higher-kinded type is F, which you can think of as standing in for Array or Map or any type that could implement map. Expressions like HKT<F, A> can be thought of as F<A> where the higher-kinded type F has been applied to the type parameter A. (I'm doing some hand waving here - F is actually a type-level tag. But the simplified view works to some extent.)
You can create an implementation of Functor for any type. But there is a catch: you need to define your type in terms of HKT so that it can be used as a higher-kinded type. In flow-static-land in the module Arr.js we see this higher-kinded version of the array type:
class IsArr {} // type-level tag, not used at runtime
export type ArrV<A> = Array<A>; // used internally
export type Arr<A> = HKT<IsArr, A>; // the HKT-compatible array type
If you do not want to use Arr<A> in place of Array<A> everywhere in your code then you need to convert using inj: (a: Array<A>) => Arr<A> and prj: (fa: Arr<A>) => Array<A>. inj and prj are type-level transformers - at runtime both of those functions just return their input, so they are likely to be inlined by the JIT. There is no runtime difference between Arr<A> and Array<A>.
A Functor implementation for Arr looks like this:
const arrFunctor: Functor<IsArr> = {
function map<A, B>(f: (a: A) => B, fa: Arr<A>): Arr<B> {
const plainArray = prj(f)
const mapped = plainArray.map(f)
return inj(mapped)
}
}
In fact the entire Arr.js module is an Arr implementation for Functor, Foldable, Traversable, and other useful type classes. Using that implementation with polymorphic code looks like this:
import * as Arr from 'flow-static-land/lib/Arr'
import { type Foldable } from 'flow-static-land/lib/Foldable'
import { type Functor } from 'flow-static-land/lib/Functor'
import { type HKT } from 'flow-static-land/lib/HKT'
type Order = { items: string[], total: number }
// this code is polymorphic in that it is agnostic of the collection kind
// that is given
function computeTotal<F> (
f: Foldable<F> & Functor<F>,
orders: HKT<F, Order>
): number {
const totals = f.map(order => order.total, orders)
return f.reduce((sum, total) => sum + total, 0, totals)
}
// calling the code with an `Arr<Order>` collection
const orders = Arr.inj([{ items: ['foo', 'bar'], total: 23.6 }])
const t = computeTotal(Arr, orders)
computeTotal needs to apply map and reduce to its input. Instead of constraining the input to a given collection type, computeTotal uses its first argument to constrain its input to types that implement both Foldable and Functor: f: Foldable<F> & Functor<F>. At the type-level the argument f acts as a "witness" to prove that the given collection type implements both map and reduce. At runtime f provides references to the specific implementations of map and reduce to be used. At the entry point to the polymorphic code (where computeTotal is called with a statically-known collection type) the Foldable & Functor implementation is given as the argument Arr. Because Javascript is not designed for type classes the choice of Arr must be given explicitly; but Flow will at least throw an error if you try to use an implementation that is incompatible with the collection type that is used.
To round this out here is an example of a polymorphic function, allItems, that accepts a collection, and returns a collection of the same kind. allItems is agnostic of the specific type of collection that it operates on:
import { type Monad } from 'flow-static-land/lib/Monad'
import { type Monoid, concatAll } from 'flow-static-land/lib/Monoid'
import { type Pointed } from 'flow-static-land/lib/Pointed'
// accepts any collection type that implements `Monad` & `Monoid`, returns
// a collection of the same kind but containing `string` values instead of
// `Order` values
function allItems<F> (f: Monad<F> & Monoid<*>, orders: HKT<F, Order>): HKT<F, string> {
return f.chain(order => fromArray(f, order.items), orders)
}
function fromArray<F, A> (f: Pointed<F> & Monoid<*>, xs: A[]): HKT<F, A> {
return concatAll(f, xs.map(f.of))
}
// called with an `Arr<Order>` collection
const is = allItems(Arr, orders)
chain is flow-static-land's version of flatMap. For every element in a collection, chain runs a callback that must produce a collection of the same kind (but it could hold a different value type). That produces effectively a collection of collections. chain then flattens that to a single level for you. So chain is basically a combination of map and flatten.
I included fromArray because the callback given to chain must return the same kind of collection that allItems accepts and returns - returning an Array from the chain callback will not work. I used a Pointed constraint in fromArray to get the of function, which puts a single value into a collection of the appropriate kind. Pointed does not appear in the constraints of allItems because allItems has a Monad constraint, and every Monad implementation is also an implementation of Pointed, Chain, Functor, and some others.
I am personally a fan of flow-static-land. The functional style and use of HKTs result in code with better type safety than one could get with object-oriented style duck typing. But there are drawbacks. Error messages from Flow can become very verbose when using type unions like Foldable<F> & Functor<F>. And the code style requires extra training - it will seem super weird to programmers who are not well acquainted with Haskell.
I wanted to follow up with another answer that matches up with the question that you actually asked. Flow can do just what you want. But it does get a bit painful implementing functions that operate on all four of those collection types because in the case of Map the type for keys is fully generic, but for Array the key type must be number, and due to the way objects are implemented in Javascript the key type for Object is always effectively string. (Set does not have keys, but that does not matter too much because you do not need to use keys to set values in a Set.) The safest way to work around the Array and Object special cases would be to provide an overloaded type signature for every function. But it turns out to be quite difficult to tell Flow that key might be the fully-generic type K or string or number depending on the type of obj. The most practical option is to make each function fully generic in the key type. But you have to remember that these functions will fail if you try to use arrays or plain objects with the wrong key type, and you will not get a type error in those cases.
Let's start with a type for the set of collection types that you are working with:
type MyIterable<K, V> = Map<K, V> | Set<V> | Array<V> | Pojo<V>
type Pojo<V> = { [key: string]: V } // plain object
The collection types must all be listed at this point. If you want to work with an open set of collection types instead then see my other answer. And note that my other answer avoids the type-safety holes in the solution here.
There is a handy trick with Flow: you can put the keyword %checks in the type signature of a function that returns a boolean, and Flow will be able to use invocations of that function at type-checking time for type refinements. But the body of the function must use constructions that Flow knows how to use for type refinements because Flow does not actually run the function at type-checking time. For example:
function isMap ( obj: any ): boolean %checks {
return obj instanceof Map
}
function isSet ( obj: any ): boolean %checks {
return obj instanceof Set
}
function isArray ( obj: any ): boolean %checks {
return obj instanceof Array
}
I mentioned you would need a couple of type casts. One instance is in set: Flow knows that when assigning to an array index, the index variable should be a number, and it also knows that K might not be number. The same goes for assigning to plain object properties, since the Pojo type alias specifies string keys. So in the code branch for those cases you need to type-cast key to any, which effectively disables type checking for that use of key.
function set<K, V>( obj: MyIterable<K, V>, key: K, value: V ) {
if( isMap(obj) ) { obj.set(key, value); }
else if( isSet(obj) ) { obj.add(value); }
else { obj[(key:any)] = value; }
}
Your instantiateSameType function just needs a type signature. An important point to keep in mind is that you use instantiateSameType to construct the result of map, and the type of values in the collection can change between the input and output when using map. So it is important to use two different type variables for the value type in the input and output of instantiateSameType as well. You might also allow instantiateSameType to change the key type; but that is not required to make map work correctly.
function instantiateSameType<K, A, B>( obj: MyIterable<K, A> ): MyIterable<K, B> {
if( isArray(obj) ) { return []; }
else if( isMap(obj) ) { return new Map(); }
else if( isSet(obj) ) { return new Set(); }
else { return {}; }
}
That means that the output of instantiateSameType can hold any of values. It might be the same type as values in the input collection, or it might not.
In your implementation of forEach you check for the presence of obj.forEach as a type refinement. This is confusing to Flow because one of the types that make up MyIterable is a plain Javascript object, which might hold any string key. Flow cannot assume that obj.forEach will be falsy. So you need to use a different check. Re-using the isArray, etc. predicates works well:
function forEach<K, V, M: MyIterable<K, V>>( obj: M, fn: (value: V, key: K, obj: M) => any ) {
if( isArray(obj) || isMap(obj) || isSet(obj) ) {
obj.forEach((value, key) => fn(value, (key:any), obj));
} else {
for (const key of Object.keys(obj)) {
fn(obj[key], (key:any), obj)
}
}
}
There are two more issues to point out: Flow's library definition for Object.entries looks like this (from core.js):
declare class Object {
/* ... */
static entries(object: any): Array<[string, mixed]>;
/* ... */
}
Flow assumes that the type of values returned by Object.entries will be mixed, but that type should be V. The fix for this is to get values via object property access in a loop.
The type of the key argument to the given callback should be K, but Flow knows that in the array case that type will actually be number, and in the plain object case it will be string. A couple more type casts are necessary to fix those cases.
Finally, map:
function map<K, A, B, M: MyIterable<K, A>>(
obj: M, fn: (value: A, key: K, obj: M) => B
): MyIterable<K, B> {
const result = instantiateSameType( obj );
forEach(obj, (value, key)=>{
set( result, key, fn(value, key, this) );
});
return result;
}
Some things that I want to point out here: the input collection has a type variable A while the output collection has the variable B. This is because map might change the type of values. And I set up a type variable M for the type of the input collection; that is to inform Flow that the type of the callback argument obj is the same as the type of the input collection. That allows you to use functions in your callback that are particular to the specific collection type that you provided when invoking map.
Related
Given:
class Foo {}
class Bar {}
interface QueryResult<T> {
data: T;
}
function creatQueryResult<T>(data: T): QueryResult<T> {
return {data};
}
function tuple<T extends any[]>(...args: T): T {
return args;
}
I want to create and type a function using inferred types that accepts QueryResult[] or a tuple of QueryResults and a factory callback that picks data and invokes the callback like:
function createCompoundResult<T>(
queryResults: T,
callback: (queryResultDatas: ???<T>) => any
) {
const datas = queryResults.map((queryResult) => queryResult.data);
return callback(datas);
}
Note the ??? in the above code.
Usage:
const fooQueryResult = creatQueryResult(new Foo());
const barQueryResult = creatQueryResult(new Bar());
// Maybe using tuples are wrong?
const queryResults = tuple(fooQueryResult, barQueryResult);
createCompoundResult(
queryResults,
(datas) => {
const [foo, bar] = datas;
// foo should be inferred as Foo here and bar as Bar
}
);
Maybe tuples are the wrong way to go? How would you solve it?
I'm a bit of a TypeScript newbie and I have a really hard time understanding stuff like keyof, extends keyof, { [K in keyof T]: { a: T[K] } } so if your solutions include arcane magic like this, please explain it to me like I'm 5 years old.
My suggestion for createCompoundResult() is this:
function createCompoundResult<T extends any[]>(
queryResults: readonly [...{ [I in keyof T]: QueryResult<T[I]> }],
callback: (queryResultDatas: readonly [...T]) => any
) {
const datas = queryResults.map((queryResult) => queryResult.data) as T;
return callback(datas);
}
The function is generic in T, corresponding to the tuple of arguments to callback. In order to describe the type of queryResults in terms of the array/tuple type T, we want to map it to another array/tuple type where for each numeric index I of T, the element type T[I] gets mapped to QueryResult<T[I]>. So if T is [string, number], then we want queryResults to be of type [QueryResult<string>, QueryResult<number>].
You can do this via a mapped type. It looks like { [I in keyof T]: QueryResult<T[I]> }. For array-like generic types T, mapped types like [I in keyof T] only iterate over the numeric-like keys I (and skip all the other array keys like "push" and "length"). So you can imagine { [I in keyof T]: QueryResult<T[I]> } acting on a T of [string, boolean] operating on I being "0" and then "1", and T["0"] is string and T["1"] is boolean, so you get {0: QueryResult<string>, 1: QueryResults<boolean>}, which is magically interpreted as a new tuple type [QueryResult<string>, QueryResult<boolean>].
That's the main explanation, although there are a few outstanding things to mention.
First is that the compiler does not know that the array map() method will turn a tuple into a tuple, and it definitely doesn't know that queryResult => queryResult.data will turn a tuple of type { [I in keyof T]: QueryResult<T[I]> } into a tuple of type T. (See this question for more info.) It sees the output type of your queryResults.map(...) line as T[number][], meaning: some array of the element types of T. It has lost length and order information. So we have to use a type assertion to tell the compiler that the output of queryResults.map(...) is of type T, so that datas can be passed to callback.
Next, there are a few places where I've wrapped an array type AAA in readonly [...AAA]. This uses variadic tuple type syntax to give the compiler a hint that we'd like it to infer tuple types instead of array types. If you don't use that, then something like [fooQueryResult, barQueryResult] will tend to be inferred as an array type Array<QueryResult<Foo> | QueryResult<Bar>> instead of the desired tuple type [QueryResult<Foo>, QueryResult<Bar>]. Using this syntax frees us from needing to use a tuple() helper function, at least if you pass the array literal directly.
Anyway, let's make sure it works:
class Foo { x = 1 }
class Bar { y = 2 }
createCompoundResult(
[fooQueryResult, barQueryResult],
(datas) => {
const [foo, bar] = datas;
foo.x
bar.y
}
);
Looks good. I gave some structure to Foo and Bar (it's always recommended to do so even for example code) and sure enough, the compiler understands that datas is a tuple whose first element is a Foo and whose second element is a Bar.
Playground link to code
I know the implementation of how to pass infinite parameters in a function in javascript.
But how to accepts any number of objects as a parameter in function?
I have this:
function merge<T>(objA:T, objB: T){
return Object.assign(objA, objB);
}
so that I can do like this:
console.log(`${mergeObject2.age}, ${mergeObject2.name}`);
But how to declare a function if that number of objects is not known..?
for example this:
const mergeObject2 = merge({name: 'Niels'}, {age :39}, {hobby: 'all'});
Thank you.
so if I do it like this:
const mergeObject2 = merge({name: 'Niels'}, {age :39} );
console.log(`${mergeObject2.age}, ${mergeObject2.name}`);
then the output is of course:
39, Niels
But how to do it if you have more objects..?
Fundamentally, if you want to accept N parameters in TypeScript, the varying parameters will all have to have the same type. (It can be a union type, though.) For instance:
function myFunction(a: boolean, b: string, ...rest: Array<number | string>) {
// ...
}
(Array<number | string> can also be written (number | string)[].)
That function requires at least two arguments (a boolean and a string) followed by any number of number or string arguments. Note how the "rest" of the parameters are bundled up in an array, which is why they have to have the same type (number | string in the example). Of course, once you're dealing with one specific element from that array, you can use if to narrow its type.
If you don't need any fixed parameters (a and b in the example), just start with the rest parameter.
If you want to use Object.assign in the implementation, you'll need to make the first one required (or hardcode Object.assign's first argument). So your merge might be:
function merge<T extends object>(target: T, ...sources: T[]): T {
return Object.assign(target, ...sources);
}
const mergeObject2 = merge({name: "Niels"}, {age: 39}); // ¹
console.log(`${mergeObject2.age}, ${mergeObject2.name}`);
Playground link
That's basically just Object.assign but with the types slightly more restricted (I think), though.
¹ You might want to put a {} at the beginning to avoid modifying the {name: "Neils"} object.
You can make use of arguments object available in ES5.
It's just like a variable with type array, and is accessible inside a function.
It contains the values of all the arguments passed to that function.
function a(){
let sum = 0;
for(let i=0;i<arguments.length;i++){
sum += arguments[i];
}
console.log(sum);
}
a(1,2,3);
In your case to copy all objects into one, you can do something like this:
function a(){
let finalObj = {};
for(let i=0;i<arguments.length;i++){
finalObj = {...finalObj,...arguments[i]};
}
console.log(finalObj);
}
a({id:1},{name:"sam"},{age: 25});
I am trying to call an async function and specify a custom type (let's call it "CustomType"). I've specified a few random properties in that type, it's just to understand that it's something that comes from a database (content could vary depending on the retrieved item, I have many different "CustomType" for each kind of file stored in a NoSql database).
This is the code testable by using https://flow.org/try
/* #flow */
// Type that would be returned in the case of usage of "mySpecificCallFunc(): Promise<Array<CustomType>>" defined below
declare type CustomType = {
prop1: boolean,
prop2: string,
prop3: Date
}
// Generic function that does the hard work
async function asyncFunc<T>(dateStart: Date, otherArgument: string):Promise<Array<T>>
{
let r:Array<T> = [] // do a call that returns an array that must be of a specific type
return r
}
// High level function that would send parameters to the generic function "asyncFunc"
async function mySpecificCallFunc(): Promise<Array<CustomType>>
{
let r = await asyncFunc<CustomType>(new Date(), 'test')
return []
}
Cannot reference type CustomType [1] from a value position.
Flow does not want the custom type to be used.
In C# this kind of generic usage would be totally acceptable so I do not understand why it is complaining?
It gives the following error :
20: let r = await asyncFunc<CustomType>(new Date(), 'test')
^ Cannot compare boolean [1] to string [2].
References:
20: let r = await asyncFunc<CustomType>(new Date(), 'test')
^ [1]
20: let r = await asyncFunc<CustomType>(new Date(), 'test')
^ [2]
20: let r = await asyncFunc<CustomType>(new Date(), 'test')
^ Cannot reference type `CustomType` [1] from a value position.
References:
3: declare type CustomType = {
^ [1]
UPDATE:
There is currently no link between "CustomType" and the request arguments.
In the real world scenario, it looks like this :
call: DbRetrieve('type1', param1, param2)
return: [{ _type: 'type1', prop1: true, prop2:'b' }] // an array containing <CustomType> objects
As you can see, there's no "shape" that can be defined from the arguments of the function asyncFunc because the arguments are not always linked to the properties of the returned object.
It's an ORM-like call, I just wanted to be able to specify the type without doing some "brute cast", but I may be following the wrong path because the type cannot be inferred from the usage...
You can not specifying type directly on call.
Your CustomType is an object, but in code you expect a boolean
So, first of all you need to make a link between an incoming and outgoing data:
async function asyncFunc<T>(p: T):Promise<T[]> {
return []
}
<T> in the function declaration is just like declaring a variable, but p: T and :Promise<T[]> make a dependency
Second, you need to make the T a little bit narrow by async function asyncFunc<T: CustomType>.... And change your type CustomType = boolean;
After that, you just need to call await asyncFunc(true); without any typing.
UPD:
You trying to specify a type the function should return, just on the function call - it`s not a right way not on flow and not on JS at all. A function result type should be certainly declared on the function declaration point - it may be a single type of several types combination (type0 | type).
Generic types is used to make a relations between parameters and result. So, you can, for example, make a function that get a param and returns an array of same types values like function doSmt<T>(a: T): T[] {return [a];}
I`m not sure what exactly you trying to do, but maybe you need something like this:
type CustomType<A, B> = {
prop0: A,
prop1: B,
}
function asyncFunc<C: string, D: number>(foo: C, bar: D): CustomType<C, D> {
// some actions
}
Assigning to (or asserting) never at the end of a function is a technique used in Typescript in order to force exhaustive checks at compile time.
For the compiler to detect this, however, it requires explicit strings to check against for determining if the function definitively returns before the assignment/assertion of never.
Would it be possible to introduce some sort of typed variation of Object.freeze that only works on object literals, and further up the chain, so that something like the following could be done?
Even better, is there a way to create an interface wherein the keys are automatically those of each of Action.type (in this example)? If that were the case - actionMap could simply be declared as that interface, which would force the check at compiletime.
Both are solutions to the same problem... given only a discriminated union, is it possible to do exhaustiveness checks like this, at compiletime, without needing to use explicit strings in the function?
interface Increment {
type: 'increment'
}
interface Decrement {
type: 'decrement'
}
type Action = Increment | Decrement
const inc: Increment = { type: 'increment' };
const dec: Decrement = { type: 'decrement' };
//this would be a typescript variation
const actionMap = Object.freeze({
[inc.type]: n => n + 1,
[dec.type]: n => n-1
});
function doAction(action: Action, val: number): number {
if(actionMap[action.type]) {
return actionMap[action.type](val);
}
//this would error at compile time if the above checked failed
const _exhaustiveCheck: never = action;
}
console.log(doAction(inc, 1));
console.log(doAction(dec, 1));
There is a fairly straight forward way to make a map that guarantees that it has a value for each case in a discriminated union. You simply have to set it so that the type of its keys is the discriminated union identifier.
type ActionMap = {
[P in Action["type"]]: (val:number)=>number
};
You can then implement this interface which will look something like this:
var map: ActionMap = {
decrement: n => n - 1,
increment: n=> n + 1
}
Edit: After a bunch of messing around I found a much more versatile and powerful solution that lets you not only type the keys of the discriminated union values but also allow you to type the payload.
First: Define your union in the form of key:type pairs. (I think this is cleaner to read anyway)
type Actions = {
"increment": { incrementValue: number }
"decrement": { decrementValue: number }
}
Second: Create an Action Discriminated Union from that map. This isn't the clearest code in the world, what it does is for each key value pair in ActionsMap create a new type by adding a type value {type:key} then sum all those types together to create your Discriminated Union.
type Action = {
[P in keyof Actions]: { type: P } & ActionsMap[P]
}[keyof Actions];
Third:Create a type for your map
type ActionsMap = {
[P in keyof Actions]: (val:number,action:Actions[P])=>number
}
Forth: Enjoy your entirely type safe action/reducer map!
const map:ActionsMap = {
decrement: (val, action) => val + action.decrementValue,
increment: (val, action) => val + action.incrementValue,
}
Fair warning. This very much pushes the limits on what the typescript definition can do and I personally have been bitten by relying on some of typescripts fringe behavior only to have it be changed in the next version.
I wrote a reduce function for Iterables and now I want to derive a generic map that can map over arbitrary Iterables. However, I have encountered an issue: Since Iterables 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:
pass the identity element/concat function explicitly const map = f => id => concat => xs (this is verbose and would leak internal API though)
only map Iterables that implement the monoid interface (that were cool, but introducing new types?)
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 Strings and Numbers 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.
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.
Pass the identity element/concat function explicitly const map = f => id => concat => xs
Yes, this is almost always necessary if the xs parameter doesn't expose the functionality to construct new values. In Scala, every collection type features a builder for this, unfortunately there is nothing in the ECMAScript standard that matches this.
only map Iterables that implement the monoid interface
Well, yes, that might be one way to got. You don't even need to introduce "new types", a standard for this already exists with the Fantasyland specification. The downsides however are
most builtin types (String, Map, Set) don't implement the monoid interface despite being iterable
not all "mappables" are even monoids!
On the other hand, not all iterables are necessarily mappable. Trying to write a map over arbitrary iterables without falling back to an Array result is doomed to fail.
So rather just look for the Functor or Traversable interfaces, and use them where they exist. They might internally be built on an iterator, but that should not concern you. The only thing you might want to do is to provide a generic helper for creating such iterator-based mapping methods, so that you can e.g. decorate Map or String with it. That helper might as well take a builder object as a parameter.
rely on the prototype or constructor identity of ArrayIterator, StringIterator, etc.
That won't work, for example typed arrays are using the same kind of iterator as normal arrays. Since the iterator does not have a way to access the iterated object, you cannot distinguish them. But you really shouldn't anyway, as soon as you're dealing with the iterator itself you should at most map to another iterator but not to the type of iterable that created the iterator.
Where are the prototypes of ArrayIterator/StringIterator/...?
There are no global variables for them, but you can access them by using Object.getPrototypeOf after creating an instance.
You could compare the object strings, though this is not fool proof as there have been known bugs in certain environments and in ES6 the user can modify these strings.
console.log(Object.prototype.toString.call(""[Symbol.iterator]()));
console.log(Object.prototype.toString.call([][Symbol.iterator]()));
Update: You could get more reliable results by testing an iterator's callability of an object, it does require a fully ES6 spec compliant environment. Something like this.
var sValues = String.prototype[Symbol.iterator];
var testString = 'abc';
function isStringIterator(value) {
if (value === null || typeof value !== 'object') {
return false;
}
try {
return value.next.call(sValues.call(testString)).value === 'a';
} catch (ignore) {}
return false;
}
var aValues = Array.prototype.values;
var testArray = ['a', 'b', 'c'];
function isArrayIterator(value) {
if (value === null || typeof value !== 'object') {
return false;
}
try {
return value.next.call(aValues.call(testArray)).value === 'a';
} catch (ignore) {}
return false;
}
var mapValues = Map.prototype.values;
var testMap = new Map([
[1, 'MapSentinel']
]);
function isMapIterator(value) {
if (value === null || typeof value !== 'object') {
return false;
}
try {
return value.next.call(mapValues.call(testMap)).value === 'MapSentinel';
} catch (ignore) {}
return false;
}
var setValues = Set.prototype.values;
var testSet = new Set(['SetSentinel']);
function isSetIterator(value) {
if (value === null || typeof value !== 'object') {
return false;
}
try {
return value.next.call(setValues.call(testSet)).value === 'SetSentinel';
} catch (ignore) {}
return false;
}
var string = '';
var array = [];
var map = new Map();
var set = new Set();
console.log('string');
console.log(isStringIterator(string[Symbol.iterator]()));
console.log(isArrayIterator(string[Symbol.iterator]()));
console.log(isMapIterator(string[Symbol.iterator]()));
console.log(isSetIterator(string[Symbol.iterator]()));
console.log('array');
console.log(isStringIterator(array[Symbol.iterator]()));
console.log(isArrayIterator(array[Symbol.iterator]()));
console.log(isMapIterator(array[Symbol.iterator]()));
console.log(isSetIterator(array[Symbol.iterator]()));
console.log('map');
console.log(isStringIterator(map[Symbol.iterator]()));
console.log(isArrayIterator(map[Symbol.iterator]()));
console.log(isMapIterator(map[Symbol.iterator]()));
console.log(isSetIterator(map[Symbol.iterator]()));
console.log('set');
console.log(isStringIterator(set[Symbol.iterator]()));
console.log(isArrayIterator(set[Symbol.iterator]()));
console.log(isMapIterator(set[Symbol.iterator]()));
console.log(isSetIterator(set[Symbol.iterator]()));
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-shim/0.35.1/es6-shim.js"></script>
Note: included ES6-shim because Chrome does not currently support Array#values
I know this question was posted quite a while back, but take a look at
https://www.npmjs.com/package/fluent-iterable
It supports iterable maps along with ~50 other methods.
Using iter-ops library, you can apply any processing logic, while iterating only once:
import {pipe, map, concat} from 'iter-ops';
// some arbitrary iterables:
const iterable1 = [1, 2, 3];
const iterable2 = 'hello'; // strings are also iterable
const i1 = pipe(
iterable1,
map(a => a * 2)
);
console.log([...i1]); //=> 2, 4, 6
const i2 = pipe(
iterable1,
map(a => a * 3),
concat(iterable2)
);
console.log([...i2]); //=> 3, 6, 9, 'h', 'e', 'l', 'l', 'o'
There's a plethora of operators in the library that you can use with iterables.
There's no clean way to do this for arbitrary iterable. It is possible to create a map for built-in iterables and refer to it.
const iteratorProtoMap = [String, Array, Map, Set]
.map(ctor => [
Object.getPrototypeOf((new ctor)[Symbol.iterator]()),
ctor]
)
.reduce((map, entry) => map.set(...entry), new Map);
function getCtorFromIterator(iterator) {
return iteratorProtoMap.get(Object.getPrototypeOf(iterator));
}
With a possibility of custom iterables an API for adding them can also be added.
To provide a common pattern for concatenating/constructing a desired iterable a callback can be provided for the map instead of a constructor.