如何映射任意Iterables?
Posted
技术标签:
【中文标题】如何映射任意Iterables?【英文标题】:How to map over arbitrary Iterables? 【发布时间】:2017-01-18 10:09:48 【问题描述】:我为Iterable
s 编写了一个reduce
函数,现在我想派生一个可以映射任意Iterable
s 的通用map
。但是,我遇到了一个问题:由于Iterable
s 抽象了数据源,map
无法确定它的类型(例如Array
、String
、Map
等)。我需要这种类型来调用相应的标识元素/concat 函数。想到了三个解决方案:
-
显式传递标识元素/concat 函数
const map = f => id => concat => xs
(这很冗长,但会泄漏内部 API)
仅映射实现 monoid 接口的 Iterable
s(很酷,但引入了新类型?)
依赖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 在这方面巧合地等效。
编辑:我这样做的原因似乎很混乱:我想使用可迭代/迭代器协议来抽象迭代细节,以便我的折叠/展开和派生映射/过滤器等功能是通用的。问题是,如果没有身份/连接协议,您就无法做到这一点。而我依赖原型身份的小“黑客”并没有成功。
@redneb 在他的回复中提出了一个很好的观点,我同意他的观点,不是每个可迭代的也是“可映射的”。然而,牢记这一点,我仍然认为以这种方式利用协议是有意义的——至少在 javascript 中——直到将来的版本中可能存在用于这种用途的可映射或收集协议。
【问题讨论】:
你指的那些Iterable
/ArrayIterator
/StringIterator
接口的来源是什么?它们来自一些标准的 javascript 框架吗?你自己定义了吗?
没有ArrayIterator
或StringIterator
原型,有迭代协议:developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
@micnic @redneb [].values()
在我的 chromium 浏览器中记录 ArrayIterator
。这仅仅是 chrome 特有的行为吗?
("")[Symbol.iterator]()
记录StringIterator
。
这是您要找的吗? ecma-international.org/ecma-262/6.0/…
【参考方案1】:
我以前没有使用过iterable protocol,但在我看来,它本质上是一个旨在让您使用for
循环遍历容器对象的接口。问题是您试图将该接口用于它不是为它设计的东西。为此,您需要一个单独的接口。可以想象一个对象可能是“可迭代的”但不是“可映射的”。例如,假设在一个应用程序中我们正在使用二叉树,并且我们通过按 BFS 顺序遍历它们来实现它们的可迭代接口,只是因为该顺序对于这个特定的应用程序有意义。通用地图如何为这个特定的迭代工作?它需要返回一个“相同形状”的树,但是这个特殊的可迭代实现没有提供足够的信息来重建树。
所以解决这个问题的方法是定义一个新接口(称它为Mappable
、Functor 或任何你喜欢的名称),但它必须是一个独特的接口。然后,您可以为有意义的类型(例如数组)实现该接口。
【讨论】:
当时没看懂你的回答。map
是一个形成函子的操作,函子必须保留它们映射的数据的结构。可迭代是映射的先决条件,但这还不够。谢谢!【参考方案2】:
您可以比较对象字符串,但这并不是万无一失的,因为在某些环境中存在已知错误,而在 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,因为Chrome 目前不支持Array#values
【讨论】:
当然,我可以这样做,但我真的不应该这样做。这太“hacky”了,抱歉。无论如何感谢您的贡献! 我不会称它为“hacky”(否则 99% 的库都是“hack”),而是“不可靠”。 :)【参考方案3】:对于任意可迭代,没有干净的方法可以做到这一点。可以为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。
为了提供连接/构造所需迭代的通用模式,可以为映射提供回调而不是构造函数。
【讨论】:
这确实有效:Object.getPrototypeOf(Array.prototype[Symbol.iterator]()).isPrototypeOf([].values())
和 Object.getPrototypeOf((new Set)[Symbol.iterator]()).isPrototypeOf(new Set([1]).values())
。谢谢!【参考方案4】:
显式传递标识元素/concat 函数
const map = f => id => concat => xs
是的,如果xs
参数没有公开构造新值的功能,这几乎总是必要的。在 Scala 中,每个集合类型都有一个 builder 用于此,不幸的是,在 ECMAScript 标准中没有与此匹配的内容。
只映射实现了monoid接口的Iterables
嗯,是的,这可能是一种获得方式。您甚至不需要引入“新类型”,Fantasyland specification 已经存在一个标准。然而,缺点是
大多数内置类型(String
、Map
、Set
)尽管是可迭代的,但并未实现 monoid 接口
并非所有“可映射”都是幺半群!
另一方面,并非所有可迭代对象都必须是可映射的。尝试在任意可迭代对象上编写 map
而不回退到 Array
结果注定要失败。
因此,只需寻找Functor
或Traversable
接口,并在它们存在的地方使用它们。它们可能在内部构建在迭代器上,但这与您无关。您可能想要做的唯一一件事是提供一个通用帮助器来创建这种基于迭代器的映射方法,以便您可以例如用它装饰Map
或String
。该帮助器还不如将构建器对象作为参数。
依赖于ArrayIterator、StringIterator等的原型或构造函数身份
这是行不通的,例如类型化数组使用与普通数组相同类型的迭代器。由于迭代器没有办法访问被迭代的对象,因此您无法区分它们。但无论如何你真的不应该,一旦你处理迭代器本身,你最多应该映射到另一个迭代器,而不是映射到创建迭代器的可迭代类型。
ArrayIterator/StringIterator/...的原型在哪里?
它们没有全局变量,但您可以在创建实例后使用Object.getPrototypeOf
访问它们。
【讨论】:
"你甚至不需要引入“新类型”——我并不是要指定新类型,而是为内置函数实现它们,例如class MonoidalString extends String concat() empty()
。它看起来很奇怪,但功能非常强大。感谢您的回答!
我想最好使用 polyfill String.empty
,即使那是在修改内置函数。
如果我得到你的祝福我会修改它们:D
我祝福你这样做 :-) 某些库与 .empty
的 Fantasyland 含义相冲突的可能性相对较小。
“一旦你处理迭代器本身,你最多应该映射到另一个迭代器” - 我在自己的 response 中就是这样做的。这是一个令人兴奋的话题。【参考方案5】:
我知道这个问题是很久以前发布的,但请看一下 https://www.npmjs.com/package/fluent-iterable
它支持可迭代地图以及大约 50 种其他方法。
【讨论】:
【参考方案6】:使用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'
库中有大量可用于迭代的运算符。
【讨论】:
以上是关于如何映射任意Iterables?的主要内容,如果未能解决你的问题,请参考以下文章