前言
最近遇到了函数式编程的柯理化函数,于是对着这块“硬骨头”硬着皮头读下去...。经过一番边读边学、消化、学习...最后,开始感到舒适,于是我想谈谈柯理化函数的新体会(那些许多在网上解释如何阅读函数的柯理化,开始是让我这样的新手看的云里雾里,于是写了这么一个故事,为了新手也为了自己一次学习的记录吧)
对于柯理化函数的理解
函数式编程,是编程的范式(paradigm)的一种。
类似,水果里有分很多种类,苹果、香蕉、橙子...;在编程的世界里,也有分:命令式编程、逻辑式编程、函数式编程...等等。
而函数式编程,我认为是水果界里的榴莲,所谓萝卜青菜,各有所爱,而作为榴莲,在水果的争议也是有不少的,喜欢的人会很着迷,讨厌的人会相当讨厌!
因此,每每看到屏幕前出现函数式编程时,隐约满满一屏榴莲的气息,就像不爱吃榴莲的人看到榴莲的样子,要吐了的感觉。
无论哪种范式,我觉得这仅仅是一种从编程里面认识世界的方法论吧。
小明的故事(理解函数式编程):
你让小明帮你做家务。按照一般的做法,你会告诉小明所有要做的家务内容、几点要做什么:比如:
上午 7: 00 做早餐,
上午 7: 30浇花,
上午 8: 00 出门买菜,
上午 9: 00 打扫
...
你一下子把这些“任务清单”列出来,就给小明去做了。
任务清单: task1做任务1,task2任务2,task3任务3...;
那么现在创建了任务安排 函数 Job, 以及各个任务task 的函数是这样子的:
Job(task1,task2,task3,task4,task5,task6 ...)
//请做 task1 task2 task3 task4 task5 task6 ...等等工作
小明收到了工作Job,于是就开始去干活了...
后来由于你每天都要写这么一大堆繁琐任务task 到 Job 里面,你发现而且有些任务是固定不变的,有些任务又偶然又不需要做,于是你想,简单点、智能点。
把所有task任务写出太繁琐了...
比如:
- 有些任务上午不能做好会影响下午的安排
所以,希望对于任务 job() 能有先安排上午做的事,等下午了看情况 再安排下午做的事的功能;- 对于一些每天肯定都能完成的事,没必每天重复都写出来,比如:做饭时间 每天 上午 7: 00 做早餐, 每天 上午 12: 00做午饭 这样固定化
于是,聪明如你。
定义早上的任务必做taskA taskB taskC
(例如做早餐、买菜、洗衣服)那么,创建新函数 MorningJob
表示“早上必做的家务:做早餐、买菜、洗衣服,统称为 MorningJob,简称“晨活”好了 ”
let MorningJob = Job(taskA,taskB,taskC)
// 把这三个任务“封装”一起,
然后再 嵌套进Job 函数里 :
Job(MorningJob, taskD,taskE,taskF)
//请做 晨活 外加 taskD,taskE,taskF 的家务
出现了!这就是函数式编程,函数嵌套函数了(把函数MoringJob()
作为函数Job()
的参数)!
以后就这么干吧,后面还有更多的任务哦,小明。
等等,你说,“晨活”这个内容只写一次能记住?小明确实看一次就用心记住了。在代码角度,就是用函数的“闭包”来保存任务数据了。
既然小明这么用心,那么建立更多的表示任务组的函数
let Job1 = Job(task1,task2,task3); // 任务组1 ,做task1,task2,task3
let Job2 = Job(task4,task5,task6); // 任务组2,做task4,task5,task6
....
以后就把各种函数嵌套,那就可以了:
Job(Morning, Job1, Job2 ) // 请做‘晨活’,任务组1,任务组2 的工作;
当然,进一步安排工作了,可能会出现有这样的情况:
Job(Job(Morning, Job1, Job2 ), task4)
// 请做‘晨活’,任务组1,任务组2 的工作,然后再做task4 的任务
Job(Job(Job(Morning, Job1, Job2 ), task4), Job3,task5)
//请做‘晨活’,任务组1,任务组2 的工作,然后再做task4 的任务,后再做任务组3和task5任务
小结
好了,函数式编程的嵌套就开始越来越多了...阅读有些困难了,但没有关系,起码先明白了“函数式编程”就是这么一个封装的概念:把任务模块化,封装了不常变化的部分,剩下再传入其他常变化的部分。
柯理化
接下来轮到柯里化出场了。
在编程的世界里,说到 柯理化(currying), 指的是函数式编程里的一个特性。
本来的柯里化仅仅是一个数学上的一个技巧,它能化简函数然后方便函数利用数学“结合律”而已。
即它是一个数学技巧在编程的世界里的应用。
柯理化的目标是
利用模块化思想处理多参函数,通过组合函数减少每个函数的入参数量,从而提高代码的可阅读性及可维护性。
柯理化的应用
可以把每次函数调用的参数存储起来!直到说明记录结束,需要做最终计算。
存参数的功能就是前面说过利用了 “闭包”。
把多个参数的函数,化简成带单一个参数函数的形式;化作一个参数的形式函数,就可以方便做函数的嵌套了。
回到上面的家务例子,使用函数式编程,情况就变成这样
Job(task1,task2,task3,...)
,要函数式编程 会有 Job(Job(Job(task1), task2), task3)
的情况,读代码和写代码的人也难受。
柯理化 Job函数后
Job(Job(Job(task1),task2),task3)//柯理化前
Job(task1)(task2)(task3) //柯理化后原来三层的嵌套变成了一层
这么来看,是不是顺眼了
(在 ES6 的 写法箭头函数定义更加明显地 Job = task1 =>task2 =>task3=>{ }
)
柯理化后好处就是:方便阅读任务、让代码更加优雅简洁、以及提高效率
说到底,从小明自身来看,他的任务逻辑并没有变化,任务给多少还是得干多少,效率不变也不需要改进工作能力(改代码本来逻辑),但是你的角度上,安排的任务更加顺心,能更加合理地安排工作(例如增加小明工作量)反而使得一天下来要做的事效率提高了!
小结
函数式编程利用闭包的机制,把一些内容事先存储和处理了,等到后期需要的时候拿来用即可。
柯理化,就是把函数式编程里原本要嵌套函数形式“简化”,成为带一个参数的函数表示形式。每次带一个参数,多次传入。
再说业务上一个例子
目的:代码做一个创建表格里一个单元格的函数
我觉得应用的特点是以最小单位为目标来定义,一个表格最小单位的单元格。这就是函数编程的想要做什么事,就直接描述具体内容,积少成多。
实现一个格子的处理代码,写一个柯理化函数:
我们定义了格子单元名为 Cell
的函数, 参数FormConfig
表示这个格子的格式,参数Value
表示这个格子的内容,
let Cell = ({FormConfig}) => Value => { ... } //格子函数Cell
应用1 创建数据单元格
接下来利用这个Cell
函数,去创建一个普通数据单元格实例
想用这个格子函数创建数据类型的格子dataCell
时,那么我们就在Cell
函数里传入dataFormConfig
数据格子格式(小数点位数),DataValue
(数据格子的内容)
let dataCell = Cell(dataFormConfig)(DataValue) // 创建数据类型格子dataCell
1 第一次传入的参数为表格属性
FormConifg
2 第二次传入的参数为表格要处理的业务值 Value
3 对于某些未完全传入的参数,就不能返回最终结果;但对于一些先传入的参数先计算并利用闭包保存,等待剩参数传入后完成最后的输出。
应用2 创建标题单元格:
接下来利用这个Cell
函数,去创建一个标题栏功能格子实例:
我们传入作为标题栏的配置数据titleConfig
,与该标题格子的数据titleName
let titleCell = ({titleConifg})=> titleName => {...} // 箭头函数写法
let titleCell = Cell({titleConfig})(titleName) //或者普通函数写法
当然,以上应用你可以说不如直接创建
Cell = (FormConfig, Value)
就写多个参数,这样不就更加简便了吗?何必大费周章。
是的,当参数少的时候,或者数据类型不那么有“含义”的时候,可以这么做。
但是当参数多了,或者出现是类似“晨活” 这样固定的需求,以及上例则把“数据的配置”和“数据的值”作为分类。
总之,面对复杂的函数,函数式编程的优点就出现了。
(函数式编程基本上是几行代码,实现其他范式的几十行甚至上百行的代码。所以往往别人阅读的时候就这么头痛了。假设,你要实现一个功能,写的代码量就要上千行,那么光是你在键盘上敲下这么多代码的时间就已经要花1个星期的时间。但是,如果你用了函数式编程,代码量一下子就回到几百行,三两天就完成了,工作效率就提高了,这就跟你安排小明做家务一样少些很多 task 的情况类似。)
体会一下:
多参数
带有“含义”的参数
其中对这个“含义”,是不是有内味了~
“含义”就是各个参数的传入“得到了管理”,它并不是数据本身的属性,但在的业务上类型这些为了实现管理而归类出来的属性。
另外,在传入函数的“时间上”也做了考虑,例如dataCell
, 其中DataValue
是由用户填写数据的时候触发的;但是填数据之前格子先要渲染的配置。所以可以实现
let onChange = Cell(dataFormConfig)
, 再判断onChange
以及DataValue
, 先渲染格式等有数据后再填入数据,最后才创建出格子onChange(DataValue)
;
而一般阅读函数:
let a =>b=>c=>d=>{ ... }
阅读函数链:从左到右传入参数到 最右边的函数处理内容,
总结
- 参数的梳理:让不同类型的参数分别输入,比如业务上的数据与配置数据分别输入
- 能模块化:让函数先执行一部分,在利用闭包保存,实现封装,再等候剩下部分由传入的参数继续运行,最后返回结果;
感受
现实生活中还是有不少人觉得函数嵌套很难理解,不够直观,遇到柯理化函数后又看不明白为何这么多参数怎么用,当遇到嵌套很多层的函数,简直就是“榴莲老千层饼”了,要吐了的感觉(当然除了源码一般人也不敢这么用)。
其实,最基本柯理化是为函数嵌套服务的,它仅仅是一个化简技巧,重点前提是我们要首先理解为什么嵌套函数,之后再做对柯理化函数的阅读就比较容易理解,这就体会到柯理化的好处了。