如何映射任意 Iterables?
How to map over arbitrary Iterables?
我为 Iterable
编写了一个 reduce
函数,现在我想派生一个可以映射到任意 Iterable
的通用 map
。但是,我遇到了一个问题:由于 Iterable
s 抽象了数据源,map
无法确定它的类型(例如 Array
、String
、Map
等)。我需要这种类型来调用相应的标识 element/concat 函数。想到三个解决方案:
- 显式传递身份 element/concat 函数
const map = f => id => concat => xs
(这很冗长并且会泄漏内部 API)
- 仅映射
Iterable
实现了幺半群接口(很酷,但引入了新类型?)
- 依赖
ArrayIterator
、StringIterator
等的原型或构造函数身份
我尝试了后者,但是 isPrototypeOf
/instanceof
总是产生 false
无论做什么,例如:
Array.prototype.values.prototype.isPrototypeOf([].values()); // false
Array.prototype.isPrototypeOf([].values()); // false
我的问题:
ArrayIterator
/StringIterator
/...的原型在哪里?
- 是否有更好的方法来解决给定的问题?
编辑: [][Symbol.iterator]()
和 ("")[Symbol.iterator]()
似乎共享相同的原型:
Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]())) ====
Object.getPrototypeOf(Object.getPrototypeOf(("")[Symbol.iterator]()))
似乎无法通过原型进行区分。
编辑: 这是我的代码:
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
行 A
中的代码产生了所需的结果。然而 B
产生 Array
而不是 String
并且连接只起作用,因为 String
s 和 Number
s 在这方面恰好是等价的。
编辑: 我这样做的原因似乎很混乱:我想使用 iterable/iterator 协议来抽象迭代细节,这样我的 fold/unfold 和派生的 map/filter 等函数是通用的。问题是,如果没有 identity/concat 的协议,您将无法执行此操作。而我依赖原型身份的小 "hack" 并没有成功。
@redneb 在他的回应中提出了一个很好的观点,我同意他的观点,即并非每个可迭代对象也是 "mappable"。但是,记住这一点,我仍然认为以这种方式使用协议是有意义的 - 至少在 Javascript 中,直到可能在未来的版本中有用于此类用法的可映射或收集协议。
我以前没有使用过 iterable protocol,但在我看来,它本质上是一个旨在让您使用 for
循环迭代容器对象的接口。问题是您正试图将该界面用于其设计目的以外的用途。为此,您需要一个单独的界面。可以想象,一个对象可能是 "iterable" 而不是 "mappable"。例如,假设在一个应用程序中我们正在使用二叉树,并且我们通过以 BFS 顺序遍历它们来为它们实现可迭代接口,只是因为该顺序对这个特定应用程序有意义。通用地图如何适用于这个特定的可迭代对象?它需要 return 一棵 "same shape" 的树,但是这个特定的可迭代实现没有提供足够的信息来重建树。
所以解决这个问题的方法是定义一个新接口(称之为 Mappable
、Functor 或任何你喜欢的接口)但它必须是一个独特的接口。然后,您可以为有意义的类型(例如数组)实现该接口。
您可以比较对象字符串,尽管这不是万无一失的,因为在某些环境中存在已知错误,并且在 ES6 中用户可以修改这些字符串。
console.log(Object.prototype.toString.call(""[Symbol.iterator]()));
console.log(Object.prototype.toString.call([][Symbol.iterator]()));
更新:您可以通过测试对象的迭代器可调用性来获得更可靠的结果,它确实需要完全符合 ES6 规范的环境。像这样。
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>
注:包含ES6-shim because Chrome does not currently support Array#values
没有干净的方法可以为任意可迭代对象执行此操作。可以为built-in iterables创建地图并参考它。
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));
}
如果可以自定义迭代器,也可以添加 API 来添加它们。
要为 concatenating/constructing 提供一个通用模式,可以为映射而不是构造函数提供一个所需的可迭代回调。
Pass the identity element/concat function explicitly const map = f => id => concat => xs
是的,如果 xs
参数不公开构建新值的功能,这几乎总是必要的。在 Scala 中,每个集合类型都为此提供了一个 builder,不幸的是,ECMAScript 标准中没有任何内容与此相匹配。
only map Iterables that implement the monoid interface
嗯,是的,这可能是获得的一种方式。你甚至不需要引入 "new types",Fantasyland specification 已经有了一个标准。然而缺点是
- 大多数内置类型(
String
、Map
、Set
)尽管是可迭代的,但并未实现 monoid 接口
- 并非所有 "mappables" 甚至都是幺半群!
另一方面,并不是所有的可迭代对象都必须是可映射的。试图在任意可迭代对象上写一个 map
而不回退到 Array
结果注定要失败。
因此,只需寻找 Functor
或 Traversable
接口,并在它们存在的地方使用它们。它们可能在内部构建在迭代器上,但这不应该让您担心。你可能唯一想做的就是提供一个通用的帮助器来创建这种基于迭代器的映射方法,这样你就可以,例如用它装饰 Map
或 String
。该助手还不如将构建器对象作为参数。
rely on the prototype or constructor identity of ArrayIterator, StringIterator, etc.
那是行不通的,例如,类型化数组使用与普通数组相同的迭代器。由于迭代器没有办法访问被迭代的对象,因此您无法区分它们。但是无论如何你真的不应该,一旦你处理迭代器本身,你最多应该映射到另一个迭代器而不是创建迭代器的可迭代类型。
Where are the prototypes of ArrayIterator/StringIterator/...?
它们没有全局变量,但您可以在创建实例后使用Object.getPrototypeOf
访问它们。
我知道这个问题很久以前就发布了,但请看一下
https://www.npmjs.com/package/fluent-iterable
它支持可迭代映射以及约 50 种其他方法。
使用 iter-ops 库,您可以应用任何处理逻辑,同时只迭代一次:
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'
库中有大量运算符可用于可迭代对象。
我为 Iterable
编写了一个 reduce
函数,现在我想派生一个可以映射到任意 Iterable
的通用 map
。但是,我遇到了一个问题:由于 Iterable
s 抽象了数据源,map
无法确定它的类型(例如 Array
、String
、Map
等)。我需要这种类型来调用相应的标识 element/concat 函数。想到三个解决方案:
- 显式传递身份 element/concat 函数
const map = f => id => concat => xs
(这很冗长并且会泄漏内部 API) - 仅映射
Iterable
实现了幺半群接口(很酷,但引入了新类型?) - 依赖
ArrayIterator
、StringIterator
等的原型或构造函数身份
我尝试了后者,但是 isPrototypeOf
/instanceof
总是产生 false
无论做什么,例如:
Array.prototype.values.prototype.isPrototypeOf([].values()); // false
Array.prototype.isPrototypeOf([].values()); // false
我的问题:
ArrayIterator
/StringIterator
/...的原型在哪里?- 是否有更好的方法来解决给定的问题?
编辑: [][Symbol.iterator]()
和 ("")[Symbol.iterator]()
似乎共享相同的原型:
Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]())) ====
Object.getPrototypeOf(Object.getPrototypeOf(("")[Symbol.iterator]()))
似乎无法通过原型进行区分。
编辑: 这是我的代码:
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
行 A
中的代码产生了所需的结果。然而 B
产生 Array
而不是 String
并且连接只起作用,因为 String
s 和 Number
s 在这方面恰好是等价的。
编辑: 我这样做的原因似乎很混乱:我想使用 iterable/iterator 协议来抽象迭代细节,这样我的 fold/unfold 和派生的 map/filter 等函数是通用的。问题是,如果没有 identity/concat 的协议,您将无法执行此操作。而我依赖原型身份的小 "hack" 并没有成功。
@redneb 在他的回应中提出了一个很好的观点,我同意他的观点,即并非每个可迭代对象也是 "mappable"。但是,记住这一点,我仍然认为以这种方式使用协议是有意义的 - 至少在 Javascript 中,直到可能在未来的版本中有用于此类用法的可映射或收集协议。
我以前没有使用过 iterable protocol,但在我看来,它本质上是一个旨在让您使用 for
循环迭代容器对象的接口。问题是您正试图将该界面用于其设计目的以外的用途。为此,您需要一个单独的界面。可以想象,一个对象可能是 "iterable" 而不是 "mappable"。例如,假设在一个应用程序中我们正在使用二叉树,并且我们通过以 BFS 顺序遍历它们来为它们实现可迭代接口,只是因为该顺序对这个特定应用程序有意义。通用地图如何适用于这个特定的可迭代对象?它需要 return 一棵 "same shape" 的树,但是这个特定的可迭代实现没有提供足够的信息来重建树。
所以解决这个问题的方法是定义一个新接口(称之为 Mappable
、Functor 或任何你喜欢的接口)但它必须是一个独特的接口。然后,您可以为有意义的类型(例如数组)实现该接口。
您可以比较对象字符串,尽管这不是万无一失的,因为在某些环境中存在已知错误,并且在 ES6 中用户可以修改这些字符串。
console.log(Object.prototype.toString.call(""[Symbol.iterator]()));
console.log(Object.prototype.toString.call([][Symbol.iterator]()));
更新:您可以通过测试对象的迭代器可调用性来获得更可靠的结果,它确实需要完全符合 ES6 规范的环境。像这样。
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>
注:包含ES6-shim because Chrome does not currently support Array#values
没有干净的方法可以为任意可迭代对象执行此操作。可以为built-in iterables创建地图并参考它。
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));
}
如果可以自定义迭代器,也可以添加 API 来添加它们。
要为 concatenating/constructing 提供一个通用模式,可以为映射而不是构造函数提供一个所需的可迭代回调。
Pass the identity element/concat function explicitly
const map = f => id => concat => xs
是的,如果 xs
参数不公开构建新值的功能,这几乎总是必要的。在 Scala 中,每个集合类型都为此提供了一个 builder,不幸的是,ECMAScript 标准中没有任何内容与此相匹配。
only map Iterables that implement the monoid interface
嗯,是的,这可能是获得的一种方式。你甚至不需要引入 "new types",Fantasyland specification 已经有了一个标准。然而缺点是
- 大多数内置类型(
String
、Map
、Set
)尽管是可迭代的,但并未实现 monoid 接口 - 并非所有 "mappables" 甚至都是幺半群!
另一方面,并不是所有的可迭代对象都必须是可映射的。试图在任意可迭代对象上写一个 map
而不回退到 Array
结果注定要失败。
因此,只需寻找 Functor
或 Traversable
接口,并在它们存在的地方使用它们。它们可能在内部构建在迭代器上,但这不应该让您担心。你可能唯一想做的就是提供一个通用的帮助器来创建这种基于迭代器的映射方法,这样你就可以,例如用它装饰 Map
或 String
。该助手还不如将构建器对象作为参数。
rely on the prototype or constructor identity of ArrayIterator, StringIterator, etc.
那是行不通的,例如,类型化数组使用与普通数组相同的迭代器。由于迭代器没有办法访问被迭代的对象,因此您无法区分它们。但是无论如何你真的不应该,一旦你处理迭代器本身,你最多应该映射到另一个迭代器而不是创建迭代器的可迭代类型。
Where are the prototypes of ArrayIterator/StringIterator/...?
它们没有全局变量,但您可以在创建实例后使用Object.getPrototypeOf
访问它们。
我知道这个问题很久以前就发布了,但请看一下 https://www.npmjs.com/package/fluent-iterable
它支持可迭代映射以及约 50 种其他方法。
使用 iter-ops 库,您可以应用任何处理逻辑,同时只迭代一次:
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'
库中有大量运算符可用于可迭代对象。