Skip to content
twobin edited this page Jul 28, 2023 · 1 revision

ES6实现Curry函数

Curry函数是老掉牙的话题了,不过有ES6的新语法糖以后,可以更愚快的装逼了。 定义:

var curry = fn => (...left) => (...right) => fn(...left, ...right)

有没有很像Haskell,2333333。上面装的逼主要用到了ES6中的以下新特性: 箭头函数(Arrow Function),其他语言里一般叫Lambda表达式(Lambda Expression)。 剩余参数(Rest Parameters),因为ES6之前一般用arguments来做可变长参数,而strict mode里是不给用arguments的,于是ES6提供这个特性,可以让函数声明的时候最后一个参数是变长的,而形参可以当数组来用。 展开参数(Spread Parameters),这算是一个apply的语法糖,可以在函数调用的时候把一个数组扔进去,展开成多个参数传递。 使用:

var concat = (...args) => args.join('')
var hehe = curry(concat)('hehe', 'haha')
console.log(hehe('heihei')) // hehehahaheihei

脑洞开一下,可以实现一个curryR,就是从右往左curry:

var curryR = fn => (...right) => (...left) => fn(...left, ...right)

既然要装逼,那不妨把curry和curryR弄到Function.prototype上,发现还是有点难的,必须回归function了,不能光用Lambda表达式了。 这是因为ES6里的箭头函数设计为this固定是定义时的上下文,而不能会随着调用时而改变,即使重新用call/apply/bind也无效,既然是挂在prototype上,必须用到this,所以为了获得this就得回归function了。

Function.prototype.curry = function(...left) {
  return curry(this)(...left)
}
Function.prototype.curryR = function(...right) {
  return curryR(this)(...right)
}

装逼值大减啊……

var append = concat.curry('hehe')
var prepend = concat.curryR('hehe')
console.log(append('1')) // hehe1
console.log(prepend('1')) // 1hehe
var saySomethingTo = (word, name, end) => concat(word, ', ', name, end)
var sayHelloTo = saySomethingTo.curry('Hello').curryR('!!!')
console.log(sayHelloTo('Jim')) // Hello, Jim!!!

不过上面这个实现curry的方法虽然逼格够高,但其实有一个小小的缺陷,不知道观众老爷有没有看出来? 装逼完毕……

ES6实现递归reverse函数

今天翻到@SYSU_Joyee妹子的一篇博客逆转序列的递归/尾递归(+destructuring assignment)实现(JavaScript + ES6),瞬间觉得很好玩,然后就玩上瘾了…… 在上一篇装逼教程中介绍了ES6里的箭头函数,用箭头函数可以把代码写的非常Functional,但是有一个非常严重的问题: 所有的箭头函数都是匿名函数,所以怎么递归!!!,事实上在JS里面,函数体内是可以访问函数名本身的,函数表达式function f() {}也能获得一个只有自身才能访问的函数名f,甚至当直接把箭头函数定义成一个变量时,其内部也可以访问到,但是那个就不好玩了,所以我们还是先严格的限制:所有函数表达式和箭头函数都是匿名的,只有函数声明可以有名字。 这个问题实在太经典了,因为有一个非常著名的东西就是专门用来干这个的,它就是Y组合子(这篇文章本来是在GitHub上看到的,不过找不到那个地址了……) 首先我们装个逼,用ES6箭头函数实现一个Y组合子:

const Y = f =>
    (x => f(y => x(x)(y)))
    (x => f(y => x(x)(y)))

为什么用const不用var或者let呢?因为我们要装逼啊,要immutable(扯远了)…… 关于Y组合子我就不多说了,因为我理解也不透彻,暂时还只是搬运。 接下来就可以用箭头函数写递归了,比如说阶乘:

const factorial = Y(
    f => n => n == 0 ? 1 : n * f(n - 1)
)

无function,无return,看起来是不是很Functional啊,这货一点也不像JS了嘛。 回到原来的话题,结合解构和spread语法,可以把reverse函数实现得相当像Haskell了:

const reverse = Y(
   rev => ([x, ...xs]) =>
       x === undefined ?
       [] :
       [...rev(xs), x]
)

注意以上代码目前应该只有Firefox才能运行的,因为V8似乎不支持参数解构(即使加了--harmony)。 然而对于妹子博客里的尾递归版本,则又有新的难度了,因为我们所实现的“穷人的Y组合子”只支持单参函数,而尾递归版本是需要两个参数的,咋搞? 呵呵呵呵,作为Functional Programming装逼犯,多参函数完全是异端,必须烧死啊,所以两个参数的函数就写成curry形式,函数叠罗汉。 这个转换怎么做呢?先用最经典的多参递归函数:辗转相除求最大公约数来举个例子:

function gcd(a, b) {
    if (b == 0) return a
    return gcd(b, a % b)
}

转换成箭头函数的话大概是这样

gcd = (a, b) => b == 0 ? a : gcd(b, a % b)

由于只让使用一个参数,需要改成curry形式:

gcd = a => b => b == 0 ? a : gcd(b)(a % b)

这样这个gcd就是一个单参的curry函数了,然后用上面使用Y组合子构造递归匿名函数的方式

Y(
    _gcd => a => b =>
        b == 0 ? a : _gcd(b)(a % b)
)

这样构造出来的是一个单参函数gcd(a)(b),然后再裹一层,把它铺开成双参函数

const gcd = (a, b) => Y(
    _gcd => a => b =>
        b == 0 ? a : _gcd(b)(a % b)
)(a)(b)

这样就成功构造出了gcd(a, b) 然后如法炮制,即可构造出尾递归版本的reverse函数:

const tailReverse = seq => Y(
    rev => ([x, ...xs]) => ret =>
        x === undefined ?
        ret :
        rev(xs)([x, ...ret])
)(seq)([])

计划通

使用JavaScript实现“真·函数式编程”

其实这篇文章有点标题党了,因为函数式编程是一个非常大的课题。而标题里的“真”听起来就有一股浓浓的中二气息。 没错,这篇文章是函数式装逼系列(1)(2)的进阶版。函数式编程是很火的,然而现在网上很多入门级的JS函数式编程教程(甚至书)都太水了,以为用用forEach用用map用用reduce用用immutable就叫函数式编程了。 Too young. Too simple. 本着搞一个大新闻的目的,我又开了这个无底天坑。当然一切都是从学习与娱乐的目的出发(这两件事其实并不冲突嘛),请勿评论本文中代码的实用价值。 瑞迪?黑喂狗!

要实现“真·函数式编程”,就要玩的彻底一点,必须挥刀自宫先禁用JavaScript里的大量语言特性,目前我能想到的有:

  • 禁用var/let,所有东西都用const定义,也就是说无变量,强制immutable。
  • 禁用分号,也就是不让“顺序执行”,解除过程式编程范式。你不是不爱写分号吗,这次彻底不需要写了
  • 禁用if/else,但允许用条件表达式condition ? expr1 : expr2。
  • 禁用for/while/do-while。
  • 禁用prototype和this来解除JS中的面向对象编程范式。
  • 禁用function和return关键字,仅用lambda表达式来编程(JS里叫箭头函数)。
  • 禁用多参数函数,只允许使用单个参数,相当于强行curry化

但是为了更可读(其实也可读不到哪去),我们允许使用大量ES6的新特性,比如箭头函数(lambda表达式)、解构、参数解构等等。如果你想玩这些特性,建议使用最新版的Firefox,或者node.js 4.0或更高版本加上--harmony --harmony_modules --harmony_destructuring等参数。 因为文中会用到一些ES6的特性,可能有的同学还不太清楚,所以这里花一小点篇幅简单的挑重点介绍一下:

箭头函数 其他语言里面一般叫做lambda表达式,其实我个人当然是喜欢这个名字,但是因为ES6的语言规范里就把它管叫箭头函数,自然文中还是会尽量这么说。 箭头函数的基本定义方式是:

(参数列表) => { 函数体 }

当只有一个参数的时候,可以省略括号,写成

参数名 => { 函数体 }

当函数体是一个表达式(而不是段落)的时候,可以隐式return,写成

参数名 => 返回表达式

由于我们的“真·函数式编程”是禁用过程式编程的,不存在段落,于是你可以见到下文中几乎所有的箭头函数都是最简单的形式,例如x => x * 2。 箭头函数可以返回函数,并且在返回函数的时候,它也可以隐式return,因此可以像haskell一样构建curry风格的函数,如

const add = x => y => x + y

用传统的风格来“翻译”上面的add函数,就是

function add(x) {
    return function(y) {
        return x + y
    }
}

调用的时候自然也是使用curry风格的逐个传参add(5)(3),结果就是8。 解构 解构是现代编程语言当中一个非常非常甜的语法糖,有时候我们为了实现多返回值,可能会返回一个数组,或者一个KV,这里以数组为例

const pair = a => b => [a, b]

我们可以用解构一次性将数组中的元素分别赋值到多个变量如

const [a, b] = pair('first')('second') // a是'first',b是'second'

参数结构就是在定义函数参数的时候使用结构

const add = ([a, b]) => a + b
add([5, 3])

在add函数里面,数组[5, 3]可以被自动解构成a和b两个值。数组解构有一个高级的“剩余值”用法:

const [first, ...rest] = [1, 2, 3, 4, 5] // first是1,rest是[2, 3, 4, 5]

可以把“剩余值”解构到一个数组,这里叫rest。 关于解构语法的更多趣闻,可以看看我之前的一篇博客。 OK,前戏就到这里,下面进入主题。

实现循环

实现for循环遍历数组 命令式编程当中,循环是最基本的控制流结构之一了,基本的for循环大概是这样:

for (var i = 0; i < arr.length; i++) {
    // ...
}

看见了var i和i++了吗?因为不让用变量,所以在“真·函数式编程”当中,这样做是行不通的。 函数式语言当中使用递归实现循环。首先拆解一下循环的要素:

for (初始化; 终止条件; 迭代操作) {
    迭代体
}

使用递归来实现的话,无外乎也就是把迭代终止换成递归终止。也就是说,只要有上面4个要素,就可以构造出for循环。首先将问题简化,我们只想遍历一个数组,首先定义一个迭代函数loop

function loop_on_array(arr, body, i) {
    if (i < arr.length) {
        body(arr[i])
        loop_on_array(arr, body, i + 1)
    }
}

上面的函数有几个地方不满足“真·函数式编程”的需要:

  • 使用了function定义:这个最简单,改成箭头函数就行了
  • 使用了多个参数:这个可以简单的通过curry化来解决
  • 使用了if/else:这个可以简单的通过条件表达式来解决
  • 使用了顺序执行,也就是body(i)和loop(arr, body, i + 1)这两句代码使用了顺序执行

为了解除顺序执行,我们可以使用像“回调函数”一样的思路来解决这个问题,也就是说让body多接收一个参数next,表示它执行完后的下一步操作,body将会把自己的返回值以参数的形式传给next

const body = item => next =>
    next(do_something_with(item))

这样需要修改body是不爽的,因此可以将其进行抽象,我们写一个two_steps函数来组合两步操作

const two_steps = step1 => step2 => param =>
    step2(step1(param))

这样,上面的两行顺序执行代码就变成了

two_steps (body) (_ => loop_on_array(arr, body, i + 1)) (arr[i])

注意中间那个参数它是一个函数,而并不是直接loop(arr, body, i + 1),它所接收的是body(arr[i])的结果,但是它并不需要这个结果。函数式语言当中常常用_来表示忽略不用的参数,我们的“真·函数式编程”也会保留这个习惯。 这样通过two_steps来让两步操作能够顺序执行了,我们可以完成遍历数组的函数了

const loop_on_array = arr => body => i =>
    i < arr.length ?
    two_steps (body) (_ => loop_on_array(arr)(body)(i + 1)) (arr[i]) :
    undefined

调用的时候就是

loop_on_array ([1, 2, 3, 4, 5]) (item => console.log(item)) (0)

但是你会发现最后那个(0)其实是很丑的对吧,毕竟它总是0,还不能省略,所以我们还是可以通过构造一个新的函数来抽取递归内容

const _loop = arr => body => i =>
    // 原来的loop_on_array的内容
const loop_on_array = arr => body => _loop(arr)(body)(0)
// 调用
loop_on_array ([1, 2, 3, 4, 5]) (item => console.log(item))

实现map

在上面的遍历的代码里,我们用for循环的套路来实现了对一个数组的遍历。这个思想其实还不算特别functional,要让它逼格更高,不妨从map这个函数来考虑。 map就是把一个数组arr通过函数f映射成另一个数组result,在Haskell里面map的经典定义方式是

map :: (a -> b) -> [a] -> [b]
map f []     = []
map f (x:xs) = f x : map f xs

对于空数组,map返回的结果是空数组 对于非空数组,将第一个元素使用f进行映射,结果作为返回值(数组)的第一个元素,再对后面的剩余数组递归调用map f xs,作为返回值(数组)的剩余部分 直接将上面的代码“翻译”成JS的话,大概是这个样子

const map = f => arr =>
    arr.length === 0 ?
    [] :
    [f(arr[0])].concat(map(f)(arr.slice(1)))

利用解构语法来简化的话大概是这个样子

const map = f => ([x, ...xs]) =>
    x === undefined ?
    [] :
    [f(x), ...map(f)(xs)]

至于map的用法大家其实都是比较熟悉的了,这里就只做一个简单的例子

const double = arr =>
    map(x => x * 2)(arr)
double([1, 2, 3, 4, 5]) // 结果是[2, 4, 6, 8, 10]

实现sum

接下来需要实现一个sum函数,对一个数组中的所有元素求和,有了map的递归思想,很容易写出来sum

const sum = accumulator => ([x, ...xs]) =>
    x === undefined ?
    accumulator :
    sum (x + accumulator) (xs)
sum (0) ([1, 2, 3, 4, 5]) // 结果是15

依然会发现那个(0)传参是无比丑陋的,用一开始那个loop_on_array相同的思想提取一个函数

const _sum = accumulator => ([x, ...xs]) =>
    x === undefined ?
    accumulator :
    _sum (x + accumulator) (xs)
const sum = xs => _sum (0) (xs)

计划通。

实现reduce

比较map和sum可以发现事实上他们是非常相似的: 都是把数组拆解为(头,剩余)这个模式 都有一个“累加器”,在sum中体现为一个用来不断求和的数值,在map中体现为一个不断被扩充的数组 都通过“对头部执行操作,将结果与累加器进行结合”这样的模式来进行迭代 都以空数组为迭代的终点 也许你觉得上面的map实现并不是这个模式的,事实上它是的,不放把map按照这个模式重新实现一下

const _map = f => accumulator => ([x, ...xs]) =>
    x === undefined ?
    accumulator :
    _map (f) ([...accumulator, f(x)]) (xs)
const map = f => xs => _map (f) ([]) (xs)
map(x => x * 2)([1, 2, 3, 4, 5])

和sum的模式惊人的一致对么?这就是所谓的foldr,foldr是一个对这种迭代模式的抽象,我们把它简单的描述成:

// foldr :: (a -> b -> b) -> b -> [a] -> b
const foldr = f => accumulator => ([x, ...xs]) =>
    x === undefined ?
    accumulator :
    f (x) (foldr(f)(accumulator)(xs))

其中f是一个“fold函数”,接收两个参数,第一个参数是“当前值”,第二个参数是“累加器”,f返回一个更新后的“累加器”。foldr会在数组上迭代,不断调用f以更新累加器,直到遇到空数组,迭代完成,则返回累加器的最后值。 下面我们用foldr来分别实现map和sum

const map = f => foldr (x => acc => [f(x), ...acc]) ([])
const sum = foldr (x => acc => x + acc) (0)
map (x => x * 2) ([1, 2, 3, 4, 5]) // 结果是[2, 4, 6, 8, 10]
sum ([1, 2, 3, 4, 5]) // 结果是15

这时候你会发现foldr的定义其实就是JavaScript里自带的reduce函数,没错这俩定义是一样的,通过foldr或者说叫reduce抽象,我们实现了对数组的“有状态遍历”,相比于上面的loop_on_array则是“无状态遍历”,因为“累加器”作为状态,是在不断的被修改的(严格的说它不是被修改了,而是用一个新值取代了它)。 用foldr实现的sum非常形象,就像把摊成一列的扑克牌一张一张叠起来一样。 “有状态”当然可以实现“无状态”,不care状态不就行了吗,所以使用foldr来实现loop_on_array也是完全没问题的

const loop_on_array = f => foldr(x => _ => f(x)) (undefined)
loop_on_array (x => console.log(x)) ([1, 2, 3, 4, 5])

呃,等等,为什么输出顺序是反的?是54321呢?很明显foldr中的r就表示它是“右折叠”,从递归的角度很好理解,无外乎先进后出嘛。所以要实现“左折叠”自然也有foldl函数(这里的左折叠右折叠表示折叠的起始方向,就跟东风北风一个道理):

// foldl :: (b -> a -> b) -> b -> [a] -> b
const foldl = f => accumulator => ([x, ...xs]) =>
    x === undefined ?
    accumulator :
    foldl (f) (f(accumulator)(x)) (xs)

用它重新实现loop_on_array,注意这次f的参数顺序和foldr是相反的,这次是accumulator在前、x在后,这样能更形象的表达“左折叠”的概念

const loop_on_array = f => foldl(_ => x => f(x)) (undefined)
loop_on_array (x => console.log(x)) ([1, 2, 3, 4, 5])

循环小结

在第一个for循环的例子中,我们使用了命令式编程的思路,通过构造“顺序执行”组合函数来让“循环体”和“下一次迭代”这两个操作能够顺序执行。 这个思路毫无疑问是行得通的,但是似乎又有点命令式编程思想根深蒂固的感觉,于是在后面的例子里面,通过map、sum抽象出foldr和foldl函数,实现了“有状态遍历”。 foldr/foldl是对数组(列表)操作的一个高度抽象,它非常非常强大。 而在第一个例子实现for循环的过程中,我们费了老鼻子劲才构造出的“顺序执行”难道就这么被抛弃了?其实并没有,因为foldr/foldl抽象的是对列表的操作,而“顺序执行”则是更为普适的将两个操作的顺序进行安排的方式。至于它又有什么进一步的应用,看来只能下一篇文章才能继续写了。 下集预告 不小心发现光是循环已经写了这么多……所以我觉得还是分开写吧,下一篇文章将会介绍如何实现“局部变量”和“状态”。

使用JavaScript实现“真·函数式编程”

上一篇文章使用JavaScript实现“真·函数式编程”本来以为可以一次性写完的,结果话痨本色,没办法,继续填坑,这篇应该可以完结了,讲道理嘛。 这篇当中将介绍如何在纯函数式的限制之下实现“局部变量”和“状态”。

实现局部变量

首先考虑这样一段代码

function getName() {
    return 'name from somewhere'
}
function greeting(word) {
    var name = getName()
    return word + ', ' + name
}
console.log(greeting('Hello'))

这是一段典型的命令式编程的代码,它最主要的问题就是局部变量name。

在上一篇文章的第一个实现对数组遍历的例子当中我们已经对“顺序执行”初窥门径,通过构造了一个two_steps函数,实现两个步骤(函数)顺序执行。 在这个构造过程当中,我们得到一个重要的思路,就是在函数式语言当中,如果你想“获得”一个什么东西,就构造一个新的函数,把它参数化。对于two_steps的例子而言,“想获得”的是一个step2,就把它参数化了。 所以当需要“获得”局部变量的时候,自然而然我们会想到,把要拿的东西参数化就OK了,于是我们可以简单的这么构造:

// local :: a -> (a -> b) -> b
const local = a => f => f(a)

local函数接收两个参数,a是要“捕获”的值,f是接收或者说消费这个值的函数,用它来改造上面的代码

function greeting(word) {
    return local(getName())(name => word + ', ' + name)
}

上文当中将getName()的结果“捕获”作为后面函数的参数,实现了“局部变量”name。把上面的函数按照“真·函数式编程”规则改写:

const getName = () => 'name from somewhere'
const greeting = word => local(getName())(name => word + ', ' + name)
console.log(greeting('Hello')) // 结果是'Hello, name from somewhere'

不难发现我们这个local其实就是two_steps的简化版,区别在于two_steps的第一个step1是一个函数,而local则是一个值,如果用two_steps实现local那么就是:

const local = value => step2 => two_steps (_ => value) (step2) ()

看,我们这个local的风格,看起来非常像JS当中的“回调”的方式——事实上,因为像Haskell这样的纯函数式语言没有顺序执行,你可以认为每一行代码执行顺序是不一定的,这非常类似于在JS中我们遇到了海量异步操作的时候:异步操作的执行顺序是不一定的,所以才会用回调函数来保证“异步操作->处理结果”这个顺序。回调是一种非常朴素,非常好理解,但写起来却反人类的异步编程方式。我一直不批判浏览器和node.js里把API都用回调风格来定义,因为它很原始,大家都懂,至于后来的如Promise这些方式,也可以用回调的API轻松封装出来,咸甜酸辣,五仁叉烧,任君挑选。 OK,扯远了,也许你觉得上面的例子太过简单,下面我们来看这篇文章中真正重点的内容。

实现状态

以下的例子基本上都源自从陈年译稿——一个面向Scheme程序员的monad介绍文中搬运和改造,我从这篇文章获得了巨大的启发,也先对作者和译者表示感谢。 我们写程序的过程当中常常回用到自增ID这种东西,如果在JS里要实现一个自增ID,可能会这么写

var __guid = 0
function guid() {
    return __guid++
}

好嘛,绕了一圈,又回到刚才的话题了,局部变量(这次在闭包里面而已,本质是一样的),和二次赋值。但是经过前文的启发很容易就能用参数化的方式来解决这个问题

function guid(oldValue) {
    var newValue = oldValue + 1
    return [newValue, newValue]
}

也就是

const guid = oldValue =>
    local(oldValue + 1)(newValue => [newValue, newValue])

我们把局部变量参数化,然后把返回值改成了一个数组(因为JS里没有元组,所以只能用数组暂代),这样在需要guid的时候,需要把之前的返回值的第二个值作为参数传进去;而整个程序则需要使用一个初始值(我们管叫“种子”)来启动它。 现在假如我们有3个名字,分别要对它们用guid来编号并且输出,也就是说需要连续执行3次guid,这里涉及到的就是顺序执行以及guid的状态参数传递:

const counting = state =>
    local (guid(state)) (([id1, state1]) =>
        local (guid(state1)) (([id2, state2]) =>
            local (guid(state2)) (([id3, state3]) =>
               `Alice:${id1} Bob:${id2} Chris:${id3}`
            )
        )
    )
console.log(counting(0)) // 结果是Alice 0, Bob 1, Chris 2

也许你已经被后面谜一般的这一堆括号所惹毛了,如果你能忍着继续看下去的话也许可以真的获得这篇文章的乐趣(捂脸 对于没有副作用的函数(纯函数),不需要带上state这个参数,而对于有副作用的函数——我们称之为“操作”——这里体现为调用了guid函数的,就需要带上state这个参数。 看,state就是我们所说的“状态”,整个过程中,都把它(用数组的第二个值)揣着,当一个函数需要状态的时候就传给它,它用完了又捡回来揣着。 看起来没什么不对,但是guid(state)这个函数总是给人隐隐的不爽:state是guid自身的状态,却需要counting这个消费者在整个调用过程当中帮它传递,这是不爽的,因此不妨把guid的state参数定义为curry形式:

const guid = () => state =>
    local(state + 1)(state1 => [state1, state1])

进而counting中的local的第一步不再是一个已算出的值,而是一个curry了第一个参数(空),需要第二个参数state的guid函数,这就是curry函数的精妙之处,它让函数可以“部分地”被执行,从而能够实现演算——我们把整个演算过程像代数推导一样列出来,最后把值代入就能计算出结果,是不是很像中学的时候解代数题、物理题什么的? 于是,用了新的guid以后,local就不能应用于guid了,使用two_steps改写一下

const counting_by_steps = state => two_steps
    (guid()) (([id1, state1]) => two_steps
        (guid()) (([id2, state2]) => two_steps
            (guid()) (([id3, state3]) => `Alice:${id1} Bob:${id2} Chris:${id3}`)
            (state2))
        (state1))
    (state)
console.log(counting_by_steps(0))

这时候你会觉得更烦了,因为这次虽然我们不需要给guid()主动传递state了,但在连续多次调用two_steps的时候,却需要把state1和state2给two_steps传递下去,能不能构造一个新的two_steps函数,让它能够透明地传递state参数呢? 答案是显然的,回顾一下上文中two_steps的定义和实现:

// two_steps :: (a -> b) -> (b -> c) -> a -> c
const two_steps = step1 => step2 => param =>
    step2(step1(param))

我们想想,two_steps的param参数作用无外乎是作为“状态”传给step1,它的定义是curry化的,如果two_steps不传第三个参数,获得的就是一个内容为step1-then-step2的“部分函数”这个函数接收param参数,返回step2的结果。要让two_steps能够继续透明地使用这种“部分函数”,就是说two_steps的结果可以继续被two_steps组合,我们可以对step1和step2函数的类型进行限定

step1 :: State -> [Value, State]
step2 :: State -> [Value, State]
param :: State

其中State是状态的类型,Value是返回值的类型,在guid的例子里面,这两者都是Number。这样结合起来,新的two_steps——我们给它一个新名字叫begin——的类型限定就是

begin :: (State -> [Value, State]) -> (State -> [Value, State]) -> State -> [Value, State]

对吧,begin的两个参数step1和step2都是State -> [Value, State]类型,这跟它在只curry前两个参数,剩余param参数时的那个部分函数step3的类型(函数签名)是一样 的。

从中抽取出一个模式:State -> [Value, State],我们用一个泛型来抽象它叫做M,不难发现,guid是() -> Number -> [Number, Number]也就是() -> M类型(其State也是Number类型),用这个泛型可以把begin描述成:

begin :: M<Value> -> M<Value> -> State -> [Value, State]

这样我们可以顺利的推出begin的实现

const begin = step1 => step2 => state => {
    const [value1, state1] = step1(state)
    return step2(state1)
}

简化之,结果是

const begin = step1 => step2 => state => step2(step1(state)[1])

这和two_steps如出一辙,区别只在于,对于step2,它丢弃了step1所产生的Value,而只保留了它所产生的State。

然而我们还是需要Value啊!说丢就丢这也太不负责任了吧!这时候自然想到“再加一个中间层”,我们设计这样一个函数:它的第二个参数消费step1所产生的Value,返回step2,这个step2再去消费step1所产生的State。把这个函数命名为bind,它的类型描述如下

bind :: (State -> [Value, State]) -> (Value -> (State -> [Value, State])) -> State -> [Value, State]

使用M泛型来描述它就是

bind :: M<Value> -> (Value -> M<Value>) -> State -> [Value, State]

看,当使用bind来结合两个操作step1和build_step2的时候,step1消费掉State种子,产生一个返回值Value(并且可能产生了新的状态State)。紧接着build_step2消费了step1所返回的Value,并且它返回一个新的M也就是step2,bind函数会像begin那样,把step1所产生的新State作为参数传给step2,并且返回其结果。于是我们终于构造出了bind的实现:

const bind = step1 => build_step2 => state => {
    const [value1, state1] = step1(state)
    const step2 = build_step2(value1)
    return step2(state1)
}

转换成“真·函数式编程”,这里利用local实现局部变量:

const bind = step1 => build_step2 => state =>
    local (step1(state)) (([value1, state1]) =>
        local (build_step2(value1)) (step2 =>
            step2(state1)
        )
    )

不难发现,begin是bind的一个特例,用bind来构造它的话就是

const begin = step1 => step2 => bind (step1) (_ => step2)

非常的直白,忽略step1产生的Value,继续调用step2。现在使用bind来改进上面的多个带状态的顺序执行的代码

const returns = value => state => [value, state]
const main = state =>
    bind (guid()) (id1 =>
        bind (guid()) (id2 =>
            bind (guid()) (id3 =>
                returns(`Alice:${id1} Bob:${id2} Chris:${id3}`)
            ))) (state) [0]
console.log(main(0)) // 结果是'Alice:1 Bob:2 Chris:3'

注意上面的代码里面我们定义了一个returns函数,它接收一个Value,并且返回一个M。毫无疑问,M是比Value更“高阶”的类型,因为M当中含有State,而Value不含。

bind函数作用于M类型,因此需要returns,于是通过returns将一个Value转成M;而main函数是返回Value类型的,则通过[0]来将一个M抛弃State转回Value。

还记得刚才那句话吗: 对于没有副作用的函数(纯函数),不需要带上state这个参数,而对于有副作用的函数——我们称之为“操作”——这里体现为调用了guid函数的,就需要带上state这个参数。

因为main函数抛弃了最终的State,所以它不在有副作用,又变成纯函数了;而正是因为它抛弃了State,它自身也变成无状态的了,所以毫无疑问重新调用main(0)就会让guid清零重新开始——局部变量,作用域,生命周期,有没有发现命令式编程里面的概念在这里体现出来了?

那么,M到底是什么? 从面相对象的角度去理解,我们可以说,M是一个封装了State在里面的“操作”。从函数式编程的角度去理解,我们理解为Value是一个“值”,而M是一个“计算”。

小结

returns函数将一个Value“提升”成M类型。 begin和bind函数将两个M绑定顺序关系(begin是bind的简化版)。bind函数中的build_step2将会有一个“临时”的机会获得Value,但是用完以后又必须回到M。 [0]将M“降级”回Value——这个过程将会不可逆地丢失掉State。 我们上面的returns和bind这一对函数就实现了一个Monad——准确的说是State Monad。在Haskell里面,我们的returns叫做return,我们的bind叫做bind或者运算符>>=。这张图 via《道可叨 | Monad 最简介绍》 via《道可叨 | Monad 最简介绍》 是我所见到的一个非常形象的描述。 除了State Monad外还有很多种Monad,例如Maybe帮助Haskell实现了非常自然的错误处理,IO帮助Haskell实现了IO。在Monad的帮助之下Haskell更实现了do notation这种“顺序执行”的语法糖,可谓是Haskell的核心之一。

总结

到现在为止,循环有了,局部变量有了,状态也有了,可以说基本上已经没有写不出的程序了。当然,正经的程序是不可能这么写的,所以这两篇文章也就是分享一下我个人的学习心得,玩玩所谓“真·函数式编程”,以及——最重要的——还是装逼。 好了,最后,我也不想说什么了,只能深吸口气,紧闭眼睛,一言(tú)以蔽之: