如何对任意可迭代对象进行映射?

6
我为可迭代对象编写了一个reduce函数,现在我想得到一个通用的map函数,可以对任意可迭代对象进行映射。但是我遇到了一个问题:由于可迭代对象抽象了数据源,map无法确定它的类型(例如Array,String,Map等)。我需要这个类型来调用相应的identity元素/concat函数。有三种解决方案:
  1. 显式传递identity元素/concat函数 const map = f => id => concat => xs(这样会很啰嗦,而且会泄漏内部API)
  2. 仅映射实现monoid接口的可迭代对象(这很酷,但会引入新类型?)
  3. 依赖于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,连接只能工作,因为在这方面StringNumber是巧合等价的。
编辑:似乎有些混淆我这样做的原因:我希望使用可迭代/迭代器协议来抽象迭代细节,以便我的fold/unfold和派生map/filter等函数是通用的。问题是,你不能这样做,除非还有一个协议来处理identity/concat。我的小“hack”依赖于原型身份的想法并没有奏效。
@redneb在他的回答中提出了一个好观点,我同意他的观点,即并不是每个可迭代对象都是可映射的。但是,在记住这一点的同时,我仍然认为在JavaScript中以这种方式利用协议是有意义的,直到未来版本可能有适用于此类用法的可映射或集合协议。

你所提到的 Iterable/ArrayIterator/StringIterator 接口的起源是什么?它们来自某个标准的 JavaScript 框架吗?还是你自己定义的? - redneb
没有 ArrayIteratorStringIterator 原型,但有迭代协议:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Iteration_protocols - micnic
源代码几乎就是胡言乱语。简单说,你是试图通过迭代器“猜测”构造函数吗?不确定“原型的区分似乎不可能”是什么意思。当然,两个迭代器原型共享公共原型(几乎每个对象都一样)。 - Estus Flask
应该是一种含蓄的侮辱吗?我不明白喜欢前端框架如何影响到一个关于JS的普遍问题。这个问题没有清楚地阐述你在实现中遇到的问题(除了像map函数这样不相关的部分)。而且这个实现并没有自我记录来解释这一点,foldl函数只会增加额外的复杂性,而不会真正帮助理解它的要点。如果你试图猜测相关迭代器的正确构造函数(而不是[]),唯一的方法就是将迭代器字符串化...这看起来很脆弱。 - Estus Flask
@estus 这不是一种侮辱,而是对你评论的恰当回应。 - user6445533
显示剩余7条评论
6个回答

5
我以前没有使用过可迭代协议,但我认为它本质上是一个接口,旨在让您使用for循环迭代容器对象。问题在于,您正在尝试将该接口用于其未设计的用途。对此,您需要一个单独的接口。可以想象,一个对象可能是“可迭代的”,但不是“可映射的”。例如,想象一下,在应用程序中,我们正在使用二叉树,并通过遍历它们(例如BFS顺序)来实现它们的可迭代接口,仅因为该顺序对于这个特定应用程序是有意义的。对于这个特定的可迭代实现,通用映射如何工作?它需要返回一个“相同形状”的树,但是这个特定的可迭代实现并没有提供重构树所需的足够信息。
因此,解决此问题的方法是定义一个新接口(称之为MappableFunctor或任何您喜欢的名称),但它必须是一个独立的接口。然后,您可以为具有意义的类型(例如数组)实现该接口。

我当时没有理解你的回答。map是一种形成函数子和保持它们映射数据结构的操作。可迭代性是映射的先决条件,但不足以满足要求。谢谢! - user6445533

1

传递身份元素/连接函数的显式参数 const map = f => id => concat => xs

是的,如果xs参数没有公开构造新值的功能,这几乎总是必要的。在Scala中,每种集合类型都具有builder,但不幸的是,在ECMAScript标准中没有与之匹配的内容。

只映射实现单子接口的可迭代对象

好吧,是的,这可能是一种方式。您甚至不需要引入“新类型”,已经存在Fantasyland规范的标准。然而,缺点是

  • 大多数内置类型(StringMapSet)尽管可迭代,但并未实现单子接口
  • 并非所有“可映射”都是单子!
另一方面,并非所有可迭代对象都必定可映射。试图在任意可迭代对象上编写map而不返回Array结果注定会失败。
因此,应当寻找FunctorTraversable接口,并在其存在的情况下使用它们。它们可能在内部基于迭代器构建,但这不应该让你担心。你唯一需要做的事情就是提供一个通用的帮助程序来创建基于迭代器的映射方法,以便你可以将其应用于例如MapString之类的对象。该帮助程序也可以接受一个构建器对象作为参数。

依赖于ArrayIterator、StringIterator等的原型或构造函数标识。

这种方式行不通,例如类型化数组使用与普通数组相同类型的迭代器。由于迭代器没有访问迭代对象的方法,因此您无法区分它们。但实际上您不应该这样做,一旦您处理迭代器本身,您最多只能将其映射到另一个迭代器,而不能映射到创建迭代器的可迭代对象类型。

ArrayIterator/StringIterator/... 的原型在哪里?

对于它们来说,没有全局变量,但是在创建实例后,可以通过使用Object.getPrototypeOf来访问它们。


“你甚至不需要引入“新类型”——我并不是指要指定新类型,而是要为内置类型实现它们,例如 class MonoidalString extends String { concat() {} empty() {} }。这看起来很奇怪,但非常强大。感谢您的回答!” - user6445533
我想即使这会修改内置函数,最好还是填充String.empty - Bergi
如果你允许的话,我会修改它们:D - user6445533
我祝福你这样做 :-) 一些库与 Fantasyland 中的 .empty 意义冲突的可能性相对较小。 - Bergi
"一旦你处理迭代器本身,你最多只能映射到另一个迭代器" - 我在我的回答中正在做这件事。这是一个令人兴奋的话题。 - user6445533

0

0

您可以比较对象字符串,但这并不是绝对可靠的,因为在某些环境中已知存在错误,并且在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”了,抱歉。无论如何,感谢您的贡献! - user6445533
1
我不会称其为“hacky”(否则99%的库都是“hack”),但是可以说它“不可靠”。 :) - Xotic750

0

使用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'

在库中有大量的运算符可用于可迭代对象。


-1

对于任意可迭代对象,没有一种简单的方法来实现此操作。可以为内置可迭代对象创建一个映射并引用它。

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。

为了提供一个常见的模式来连接/构建所需的可迭代对象,可以为map提供回调函数而不是构造函数。


这确实有效:Object.getPrototypeOf(Array.prototype[Symbol.iterator]()).isPrototypeOf([].values())Object.getPrototypeOf((new Set)[Symbol.iterator]()).isPrototypeOf(new Set([1]).values())。谢谢! - user6445533

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接