本文来自【前端早读课】,内容不错,推荐给大家。

前言

今日早读文章由酷家乐@Gloria投稿分享。

正文从这开始~~

作为前端工程师,你肯定用过Array.prototype.map方法。

如果你听说过Ramda,它也提供了和Array.prototype.map方法类似的map方法。

但是这个map背后的东西可以让你看到另外一个世界,我相信,如果你不想了解Ramda,也能从这篇文章中有所收获。

下面我们进入到例子。

简单的使用

像下面这样使用这个函数。

R.map(x => x + 1, [1, 2, 3]); // [2, 3, 4]

除了数组外它还可以作用于Object:

R.map(x => x + 1, {a: 1, b: 2, c: 3}); // {a: 2, b: 3, c: 4}

你以为就完了吗?它还能作用于函数:

R.map(x => x + 1, a => a + 1); // a => (a+1)+1

哇,作用于函数真的是没想到,那还能作用于其它奇奇怪怪的东西吗?

当然可以,有很多东西从某种维度上讲都是同一类东西,关键R.map的维度是什么呢?

先别讲什么乱七八糟的,接下来咱们来看一看官方文档上都有哪些描述.

文档上都说了啥
  • 接收一个函数和一个 functor, 将该函数应用到 functor 的每个值上,返回一个具有相同形态的 functor。

  • Ramda 为 Array 和 Object 提供了合适的 map 实现,因此 R.map 适用于 [1, 2, 3] 或 {x: 1, y: 2, z: 3}。

  • 若第二个参数自身存在 map 方法,则调用自身的 map 方法。

  • 若在列表位置中给出 transfomer,则用作 transducer 。

  • 函数也是 functors,map 会将它们组合起来(相当于 R.compose)。

行了,除了2,3能看懂,其它都是啥??!!functor??transfomer??transducer??

我们找到Ramda的源码,看看这个map究竟都有哪些魔法?

看看ramda源码

隐去了一些不需要了解的逻辑,下面是代码:

var map = _dispatchable(['fantasy-land/map', 'map'], _xmap, function map(fn, functor) {
/*ramda默认处理逻辑*/
switch(Object.prototype.toString.call(functor)) {
case'[object Function]':
returnfunction() {
return fn.call(this, functor.apply(this, arguments));
};
case'[object Object]':
return _reduce(function(acc, key) {acc[key] = fn(functor[key]);
return acc;
}, {}, keys(functor));
default:
return _map(fn, functor);
}
});

先说说_dispatchable的逻辑:

function _dispatchable(methodNames, xf, fn): Function
  • _dispatchable返回的函数作为R.map的处理过程

  • 接收 3 个参数:methodNames(方法名数组),xf(transformer),fn(默认的ramda实现)

  • 如果 methodNames 中的方法名存在于传进 R.map方法的最后一个参数f上,则将该方法作为处理过程 (如 f 是数组,则使用默认的处理过程)

  • 如果最后一个参数 f 是transformer,处理结果则是:一个新的transformer

  • 如果以上3,4说的情况都没有,则使用Ramda的默认处理过程(第一个代码块注释处)

总体看下来R.map有3种处理策略(按照优先级从上到下):

  • 最后一个参数f上出现在 methodNames 中的方法

  • 根据最后一个参数 f 返回新的 transformer

  • Ramda默认处理逻辑

默认的处理逻辑就不再展开了,比较容易明白,先说说2,1放在后面讲。

transduce

进入正题之前,抛开ramda,看一个简单的栗子:

const add = (a, b) => a + b;
[1,2,3,4].reduce(add, 0); // 10

计算出一个数组中所有数字的和。

现在如果要对每个数字+1,再求和:

const add = (a, b) => a + b;
const plusOne = a => a + 1;
[1,2,3,4].map(plusOne).reduce(add, 0); // 14

上面的代码会遍历数组两次,虽然代码写起来省事了,如果数据量比较大,这个做法看起来就有些笨拙了。但是又不能改写add方法,万一别的地方也用到了add。

想办法只遍历一次:结合add和plusOne生成一个新的函数addNPlusOne:

const addNPlusOne = (acc, value) => add(acc, plusOne(value));
[1,2,3,4].reduce(addNPlusOne, 0); // 14

嗯,解决了。但是还不够通用,将add视为reducer,plusOne视为对value的预处理函数fn,通过结合fn和reducer生成一个新的reducer提供给reduce

const makeMapReducer = fn => reducer => (acc, value) => reducer(acc, fn(value));
const addNPlusOne = makeMapReducer(plusOne)(add);
[1,2,3,4].reduce(addNPlusOne); // 14
transducer

makeMapReducer(plusOne)就是一个transducer。

在之前的基础上:如果需要先筛选出小于等于2的数值,然后再给每一项+1,最后统计出数组中所有数的和。

需要再添加一个filterTransducer:

const makeFilterReducer = fn => reducer => (acc, value) => fn(value)? reducer(acc, value) : acc;
const filterTransducer = makeFilterReducer(a => a <= 2);
const addNPluslteTwo = filterTransducer(addNPlusOne);
[1,2,3,4].reduce(addNPlusltTwo); // 5

好了,也就是说如果你不使用任何第三方库,这个生成transducer的函数需要你自己去实现。

在Ramda中

在Ramda中你可以这样实现上面的栗子:

R.transduce(R.map(a => a+1), (acc, value) => acc + value, 0, [1,2,3,4]); // 14
R.transduce(R.pipe(R.map(a => a+1),R.filter(a => a <= 2),
), (acc, value) => acc+value, 0, [1,2,3,4]); // 5

再简化一点:

R.transduce(R.map(R.inc), R.add, 0, [1,2,3,4]); // 14
R.transduce(R.pipe(
R.map(R.inc),R.filter(R.gte(2)),
), R.add, 0, [1,2,3,4]); // 5

之前的例子,我们自己实现了transducer。

而对于ramda来说,很多作用于数组的api都会有默认的生成transducer的实现,比如map,filter,find等等api。

好了,好像扯远了,我们再回到R.map上,看一看这里的transformer是啥意思。

  1. 根据最后一个参数f返回新的transformer

回到开始的话题

当你调用R.transduce的时候,它会把第二个参数R.add,转化为一个对象,这个对象上存在方法@@transducer/step,这个方法返回的是R.add(acc, value)。存在方法@@transducer/step的对象就叫做transformer。

其实你可以这样理解:transformer是一个函数的载体,transformer['@@transducer/step']就是这个函数。

好了,如果当R.map的第二个参数是一个transformer的时候:

// _xwrap是ramda内部函数,用于将函数转为transformer
R.map(R.inc)(_xwrap(R.add))
// 跟下面是等价的
R.map(R.inc, _xwrap(R.add))

R.map(R.inc)其实就是上面我们说的transducer(transducer还能组合起来,不再展开了,有兴趣的同学可以加群讨论)

transducer + transformer = transformer,所以上面两行代码返回的结果依然是一个transformer,这个transformer的@@transducer/step方法最终效果是下面这样:

XMap.prototype['@@transducer/step'] = function(acc, value) {
return R.add(acc, R.inc(value));
};

这个transformer代表的就是最终的reducer函数的容器

R.transduce(R.map(R.inc), R.add, 0, [1,2,3,4]);
// 与下面是等价的
const xf = R.map(R.inc)(_xwrap(R.add));
R.reduce(xf['@@transducer/step'], 0, [1,2,3,4]);

总结一下

为了减少遍历次数,用transduce替代reduce,把之前reduce过程的前置操作比如map,filter,find等操作在一次遍历中完成。

为了实现这个transduce,以及在其上map,filter,find这种操作的可组合性,引入了transducer+transformer的概念。

这个transducer的概念最早是在Clojure里出现,有兴趣的同学可以看看:https://video.tudou.com/v/XMjMxNTY2MDgzNg==.html?__fr=oldtd

fantasyland/map
  1. 最后一个参数 f上出现在 methodNames中的方法

  2. 根据最后一个参数 f返回新的 transformer

  3. ramda默认处理逻辑

既然第2点讲完了,开始这篇文章的最后一部分,这一部分与上面讲的transducer没有任何关系,这一部分也是本文想着重介绍的。

var map = _dispatchable(['fantasy-land/map', 'map'],...)

从上面R.map的实现中可以看到,传入_dispatchable的methodsName中,第一个方法名是fantasyland/map。

如果R.map(fn, obj),obj上有fantasyland/map方法,则R.map(fn, obj)等价于 obj['fantasyland/map'](fn)

那么methodsName中另一个map和这个fantasyland/map有啥区别?为啥还有这么长的一个名字?

fantasyland规范

其实fantasyland/map这个名字是有特殊含义的,fantasyland/map没有特定的实现,不过,如果你要实现这么一个方法,你需要遵循fantasyland规范。

所谓的fantasyland规范,其实就是一个文档,这个文档里规定了一些代数结构在javascript里实现的约束

Fantasy Land Specificationaka "Algebraic JavaScript Specification"

如果你在大学有接触过《离散数学》的话,其中的一些概念会在这个规范中有具体的javascript定义,比如:二元关系(等价关系,全序关系),群,半群。当然,除了这3类数据结构,还有范畴以及在基础代数结构上衍生出来的其它结构。

类型签名

接下去我们会着重看一下与fantasy-land/map相关的定义,不过,在此之前有一些简单的类型签名,需要提前了解一下(下面的类型签名解释,是个人翻译版本,如果你有兴趣,可以直接看github上英文原版的解释):

:: :“a属于类型b”

e :: t:可以理解成:“e属于类型t”

true :: Boolean:“ true 属于 Boolean 类型”

42 :: Integer,Number :“42既属于 Integer 也属于 Number 类型”

通过类型构造函数可以构造一个新的类型

类型构造函数接受0个或多个参数

Array 就是一个类型构造函数,它接受一个类型作为参数

Array String 是存放着字符串的数组,像这几个数组都是属于 Array String :[],['foo', 'bar', 'baz']

Array(Array String) 是存放着数组的数组,存放的数组里面又存放着字符串,像这几个数组都是属于 Array(Array String):[],[[], []],[[], ['foo'], ['bar`, 'baz']]

小写字母是类型变量

类型变量可以代表任何类型,除非用胖箭头(下面有介绍)对它做类型约束

->(箭头)函数的类型构造函数

-> 是一个中缀类型构造函数,这个类型构造函数接受两个参数,箭头左边的参数是输入类型,右边的参数是输出类型

-> 可以接受0个或多个输入类型作为左边的参数。语法:() ->,中的多个类型以“ , ”分隔。一元函数输入参数旁边的括号可以省略,比如:String -> Boolean,(String, String) -> Boolean

String -> Array String 对应一类函数:接受一个 String 类型的参数,然后返回一个类型为 Array String 的值

String -> Array String -> Array String 代表着一类函数:接受一个类型为String的输入,输出一个类型为 Array String -> Array String 的函数,这个输出的函数接受一个类型为 Array String 的参数,输出类型为 Array String 的值

(String, Array String) -> Array String代表着一类函数:接受两个参数,第一个是String 类型,第二个是 Array String 类型,输出类型为 Array String 的值

() -> Number 代表着一类函数:不接受输入,返回一个类型为 Number 的值

~>(波浪箭头)方法的类型构造函数

当一个函数是一个对象的属性时,它被叫做这个对象上的“方法”。所有的“方法”都拥有一个隐含的参数类型-所在对象的类型

a ~> a -> a 代表着一类方法:是类型为 a 的对象上的方法,且这个方法接受一个类型为a 的参数,返回一个类型为 a 的值

=>(胖箭头)胖箭头用来对类型变量做类型约束

比如有这么一个方法 a ~> a -> a ,在这个方法的类型签名中,a 可以代表任何类型。Semigroup a => a ~> a -> a,而这个类型签名中就对类型变量 a 做了类型约束,使得类型 a 必须满足类型类 Semigroup 。当一个类型满足一个类型类的意思是,这个类型实现了所有类型类指定的函数/方法。

就拿这次我们要说的fantasy-land/map举例:

fantasy-land/map
fantasy-land/map解析

先不管下面这部分

Functoru'fantasy-land/map' is equivalent to u (identity)u'fantasy-land/map') is equivalent to u'fantasy-land/map''fantasy-land/map' (composition)

直接看规范中对fantasy-land/map的定义:

fantasy-land/map :: Functor f => f a ~> (a -> b) -> f b

Functor是一个类型类,f 必须满足 Functor, f a 代表了以 f 作为类型构造函数,类型 a 作为构造参数生成的类型,比如 Array String,代表字符串数组,Array 就是 f ,它满足Functor类型类。

如果一个对象,是Functor实例(具体的值)。那么这个对象上需要存在一个名为 fantasy-land/map 的方法,这个方法必须接受一个函数作为参数:

u['fantasy-land/map'](f)
// 举个例子
[1,2,3]['fantasy-land/map'](f)
f 必须是一个函数
  • 如果 f 不是一个函数,fantasy-land/map 的行为是不确定的

  • f 可以返回任何类型的值

  • 不应该检测 f 的返回类型

fantasy-land/map 方法,必须返回一个相同的Functor(比如 [1,2,3]'fantasy-land/map' 必须返回也一个数组:Array)

其实可以类比 Array.prototype.map 方法,只是换了个名字而已。

那么说了这么多,Functor 是个什么东东?除了 Array 以外,还有什么是 Functor ?

其实 Function 也是 Functor ,惊喜吗?

不卖关子了,Functor 的中文名是“函子”,接下来讲讲“函子”。

啥是函子

“函子”是范畴论中的概念,所以,在准备完全理解“函子”之前,你需要明白啥是“范畴”?

范畴

其实,在生活中,无处不充斥着范畴,只不过范畴论把这些东西抽象成了数学结构。

范畴此一概念代表着一堆数学实体和存在于这些实体间的关系。--维基百科

范畴的定义其实很简单,就是实体的集合+实体间的关系。

那么什么是“实体”?这取决于你怎么看。

从集合的角度来说,实体是 a set of values ,首先它得是一个集合(set),其次,这个集合是由有好多的值组成(value)。

还是比较抽象,再具体一点,比如:一个类型可被看作为值的集合(a set of values),类型与类型之间的关系就是函数,所以一堆类型+类型之间的函数,就是范畴。

比如有下面这些函数:

fn1 :: Number-> String
const fn1 = (a: number) => `${a}1`;
fn2 :: String-> Boolean
const fn2 = (a: string) => a === '1';
...

这些函数都是定义在Number和String上的映射关系。Number,String和Boolean,以及它们之间的映射关系,构成下面这个范畴

范畴

在范畴论中,图片中的 NUMBER , STRING 和 BOOLEAN 叫做“对象”(Object),fn1 和 fn2 叫做“态射”(Morphism), fn2 * fn1 叫做“态射复合”, NUMBER -> NUMBER 叫做单位态射。

明白什么是范畴之后,接下来说一说我们的主角:函子

函子

先来看看维基上的解释:

在范畴论中,函子是范畴间的一类映射。函子也可以解释为小范畴范畴内的态射。--维基百科

范畴和范畴也会有映射关系,如果把范畴视作一个对象时,函子就是范畴之间的态射。然后组成了一个范畴的范畴。

举个例子:考虑一个基础类型的范畴A,一个数组范畴B。

两个范畴

思考以下几个问题:

  • Number 和 Array 之间的关系

  • String 和 Array 之间的关系

  • Number 到 String 的态射与 Array 到 Array 的态射的关系

之前介绍过 Array 是类型构造函数:

  • 将 Number 传进 Array ,构造出 Array

  • 将 String 传进 Array ,构造出 Array

  • 可通过 Array 上的 map 方法会保持 Number -> String 映射到 Array<Number>->Array<String>

再回顾一下上文对函子的定义:

在范畴论中,函子是范畴间的一类映射。

上面例子中,范畴A到范畴B的映射其实就是类型构造函数 Array ,所以说, Array 就是函子。

函子

这里省去了对公式上的定义的match,争取大家对这个概念有感性的认识,如果想知道函子严谨的定义,可以看这里

回到fantasy-land/map

了解了函子的感性定义之后,回到严谨的规范上来。

之前解析 fantasy-land/map 的时候,有个定义一直没有提及,就是 Functor , fantasy-land/map 在文档中的位置其实是Functor的子标题,现在再来回顾一下。

Functor
1. u['fantasy-land/map'](a => a) is equivalent to u (identity)
2. u['fantasy-land/map'](x => f(g(x))) is equivalent to u['fantasy-land/map'](g)['fantasy-land/map'](f) (composition)

通过对比函子的公式定义,解析Functor需满足的条件(F即函子):

保持着单位态射(id即单位态射,idX即对象X上的单位态射)

保持着态射的复合

总结一下fantasyland规范中对函子的定义

如果实现一个函子,你需要在函子上实现 fantasy-land/map 方法,这个方法的类型签名应该是这样的:

fantasy-land/map :: Functor f => f a ~> (a -> b) -> f b

函子实例调用方法 fantasy-land/map 时,需同时保持单位态射和态射的复合。

结尾

这篇文章不知不觉写得有些长了,从Ramda文档->源码->transducer->fantasyland规范->范畴论->函子,算是自己完整的探索过程,希望能够带给你一些不一样的东西。

参考文章

  • JavaScript玩转Clojure大法之Transducer

  • Wikipedia 范畴论

  • Wikipedia 函子

关于本文作者:@Gloria原文:https://zhuanlan.zhihu.com/p/96059965

原创系列推荐

1. JavaScript 重温系列(22篇全)

2. ECMAScript 重温系列(10篇全)

3. JavaScript设计模式 重温系列(9篇全)

4. 正则 / 框架 / 算法等 重温系列(16篇全)

5. Webpack4 入门(上)|| Webpack4 入门(下)

6. MobX 入门(上) ||  MobX 入门(下)

7. 59篇原创系列汇总

回复“加群”与大佬们一起交流学习~

【JS】446- 你不知道的 map相关推荐

  1. 原生JS forEach()和map()遍历的区别以及兼容写法

    一.原生JS forEach()和map()遍历 共同点: 1.都是循环遍历数组中的每一项. 2.forEach() 和 map() 里面每一次执行匿名函数都支持3个参数:数组中的当前项item,当前 ...

  2. 关于移动端页面滑动报错 [InterUnableUnable to preventDefault inside passive或 fastclick.js:446 [InterUn :

    报错: fastclick.js:446 [InterUnable to preventDefault inside passivevention] event listener due to tar ...

  3. js中数组map方法的使用和实现

    js中数组map方法的使用和实现 MDN中定义 map() 方法创建一个新数组,其结果是该数组中的每个元素是调用一次提供的函数后的返回值. 语法 var new_array = arr.map(fun ...

  4. vue前端弹出新增页面testAdd,弹出页面中进行数据编辑,table编辑,行编辑等。包含JS中使用Map进行数据处理。

    使用vue写的新增页面,编辑页面等弹出页面,在页面中进行table数据的行编辑,利用JS中的Map做的实时响应的行编辑数据内容汇总计算.校验等.话不多说,上代码: require(['vue', 'z ...

  5. Node.js 中 source map 使用问题总结

    起源 Node 应用功能越来越复杂,很多业务都开始尝试使用 TypeScript 来开发.现在前端写的 JS 大部分是经过编译过程的,浏览器中通过 source map 的使用,可以很好的解决源码和编 ...

  6. js数组的map方法以及parseInt方法

    无意看到一个方法: ["1", "2", "3"].map(parseInt); 返回值为:[1, NaN, NaN]. 好奇查了下map方 ...

  7. js之 foreach, map, every, some

    js中array有四个方法 foreach, map, every, some,其使用各有倾向. 关注点一:foreach 和 map 无法跳出循环,每个元素均执行 foreach 和 map 无法跳 ...

  8. js 两个map合并为一个map_ArcGIS API for JS3.x教程二:构建第一个简单的程序

    本文衔接上文: 不睡觉的怪叔叔:ArcGIS API for JS3.x教程一:本地开发环境配置​zhuanlan.zhihu.com 一.创建简单的HTML文档 创建一个简单的HTML文档: < ...

  9. jquery中的map()方法与js中的map()方法

    1.jquery中的map()方法 首先看一个简单的实例: $("p").append( $("input").map(function(){ return $ ...

  10. 一分钟掌握js中的map方法

    目录 map是什么 map方法的结构及入参 语法糖 map一般不改变原数组 map是什么 map是操作js数组的方法,也可以说是一个函数,作用是遍历整个数组,对里面的每个值做处理再返回一个新的值. 注 ...

最新文章

  1. 循环队列的顺序存储和实现(C语言)【循环队列】
  2. C语言中 *.c和*.h文件的区别!
  3. redis简单队列java_使用Redis的简单消息队列
  4. java音乐登陆界面_第四篇——Spring音乐登录界面设计及实现(C#)
  5. IOC操作Bean管理XML方式(注入内部 bean 和 级联赋值)
  6. C语言中的编译,链接,运行简单复习
  7. SLAM会议笔记(四)Lego-LOAM
  8. Vue项目中国际化的语言切换
  9. QT 使用全局钩子监听鼠标事件和键盘事件
  10. 《炬丰科技-半导体工艺》晶片键合技术和薄膜传输技术
  11. 小牛电动Q1营收5.5亿:净利润不及预期,3个月内市值缩近五成
  12. 在线教育机构如何运营微信公众号
  13. 程序员常用远程工具有哪些?
  14. 计算机新生导论感言,大学生感言与寄语新生
  15. 工控安全PLC固件逆向一
  16. 重写和重载有什么区别
  17. [Power BI] 认识Power Query和M语言
  18. WebXR 元宇宙或将基于 Web
  19. Linux下菜鸟用XMMS(转)
  20. java 使用poi HSSFWorkbook导出xls文件 office打不开,提示文件损坏,wps能打开。

热门文章

  1. android自动切换暗色,根据环境光亮度自动切换,让 Android 10 的暗色主题更智能:Auto Dark Theme...
  2. Java纯后端生成PDF格式报表的三种方案(包含echarts图表)
  3. 【有效】vscode中markdown导出pdf报错解决: ERROR: Navigation Timeout Exceeded: 30000 ms exceeded
  4. java使用jacob操作word文档
  5. 计算机毕业设计Android网约车拼车打车叫车系统APP
  6. 电商API:淘宝/天猫获取sku详细信息
  7. Linux&Windows系统双系统
  8. html div.menus,性感的CSS菜单(Menus)
  9. 我整理的ubuntu开源软件列表,适合于极客
  10. 学会读懂 MySql 的慢查询日志