Point-free

Point-free是什么?Point实际上指的是函数的参数,Point-free是一种编程风格,这是一种在函数里面实际上没有写任何东西的函数编写方式,这种方式可以将一个函数与其他函数一起生成一个新的函数,而实际上并没有定义这个新的函数的参数是什么。比如下面这个函数就是一个简单的Point-free风格的函数:

import * as R from "ramda";

const addOne = x => x + 1;
const square = x => x * x;

const addOneThenSquare = R.compose(square, addOne);

addOneThenSquare(1); // 4

由于JS不是一门严格的函数式编程语言,所以在这里我们引入了一个叫做“ramda”的第三方库来帮我们完成一些函数式编程的事情。首先我们有两个简单的函数,这两个函数的作用分别是对传入的数进行加一和平方的操作,然后我将两个函数compose(compose是函数式编程里面的一种方法,该函数会将接收到的方法“从右到左”串联起来依次执行,将上一个函数的返回值作为下一个函数的入参)起来得到一个新的函数,这个新函数的作用是讲传入的数先加一再平方。定义这个新函数的方法就是Point-free风格。

当我们的函数中有参数时,我们的编程风格更像是命令式的,其实我们是在引导一个变量从输入值转换成输出值。但是当我们使用Point-free风格编程时,我们的编程就更偏向声明式。实际上我们是将参数的转换过程隐藏了,其实当你去问别的程序员代码是显式的好还是隐式的好,可能大多数程序员都会告诉你显式的好,因为那可以让你看到代码里发生了什么。但是在有的情况下代码可以是隐式的,比如在函数式编程中,那些一个个的“函数组件”能够给我们足够的信心处理好我们的输入输出,我们只需要关注在最后的输出是不是我们想要的,不用去关注它是怎么去做转换的,因为这对于读你代码的人来说是一些不必要立马去了解的细节。

可读性

Point-free的定义很简单,看到这里说不定一些读者就会说到,我以前也这么玩过,只不过我不知道这个就叫做Point-free。是的Point-free的定义很简单,但是要用好它就没那么简单了。设想一下,上面那个简单的例子,如果把他们的函数名改成xyz,你还能知道最后一个函数是在干嘛吗,当然这个例子比较极端,但是不妨试想下你在项目上看到过的同事起的各种奇葩的命名,然后将这一堆看起来就不知道干嘛的函数全部compose在一起,那这对于维护代码的人来说将是灾难性的。这就与函数式编程能够提高代码可读性的结论背道而驰了。所以要用好Point-free的第一件事情就是为你的函数“组件”起一个通俗易懂的好名字,让阅读者能够看着名字就知道函数在干嘛而不用去深入函数细节,还原函数式编程的初衷。

所以不要滥用Ponit-free,当项目中出现了过多的Point-free风格但又没有严格符合编程规范的话,很可能会让你的项目难以维护。

但是其实compose只是Point-free中的一种应用,还有许许多多别的应用,只要你使用得当,你的代码依旧可以清晰可读,比如下面的这个例子:

const not = (fn) => {
    return function negated(...args) {
        return !fn(...args);
    }
}

const isOdd(v) = {
    return v % 2 === 1;
}

const isEven = not(isOdd);

isEven(4);

在这个例子中,我们当然可以将isEven的定义写成return v % 2 === 0,但是其实这样的定义是给代码的阅读者增加了额外的理解负担的,你当然可以说例子里面的代码很简单,你可以一眼就看出来这个函数要做的事情,但是如果这是你公司项目中的一个复杂的判断函数呢,那是不是你就需要读两次几乎一样的代码来判断,最后发现这两个函数其实只是在做相反的事情。

这样的定义方式并不是为了我们在写代码的时候可以少写几个字母,更重要的是让我们项目里面的各个”函数组件“之间有了联系,让读代码的人一眼就能看出来,这两个函数是相反的关系,让他能够更容易的理解代码,说到底是为了提升代码的可读性。

函数的“形状”

可以注意到在使用Point-free这种编程风格时,组成最新那个函数的各个函数“组件”都是只接受一个参数并且只返回一个返回值,也就是说这里的函数都需要是数学中的一元函数。所以在函数式编程中,保持你的所有函数是一元函数是及其重要的,这可以保证它们如同乐高玩具一样拥有相同的接口,可以相互连接在一起。

当然并不是所有的函数都可以是一元函数,所以在函数式编程中有一个极其重要的东西是柯里化,当你的函数不是一元函数但是你却又想将其作为组件组合在一起时就需要通过柯里化的形式讲一个二元甚至是多元的函数转换成一个一元函数,然后再同其他函数组合在一起。也就是说在函数式编程中所有的函数都需要支持柯里化,这样才能保证各个函数之间能够正常工作。

Point-free的”高级“应用

其实Point-free的应用不仅仅是利用原本的函数来组合新的函数,更进一步的话,Point-free应该是使用一些函数的”通用组件“去组成你需要的”特殊组件“。我们还是以上面的两个函数为例:

const mod = (y) => {
    return function forX(x) {
        return x % y;
    }
}

const eq = (y) => {
    return function forX(x) {
        return x === y;
    }
}

加入我们有两个函数,其中一个函数先接受一个变量y,然后返回一个函数,这个新返回的函数接收另一个参数x,然后返回x对y取余的结果;第二个函数先接受一个变量y,然后返回一个函数,这个新返回的函数接收另一个参数x,然后返回x是否等于y的结果。请注意,这两个函数有两个很重要的特点:

  1. 它们接收的参数顺序是反直觉的,但是在函数式编程中却是必须应该这样的,原因我们可以继续往下看;
  2. 它们虽然都不是一元函数,但是都经过了柯里化处理,是由两个一元函数”嵌套“而成的,这一点在函数式编程中也极为重要,这决定了这些函数是否有相同的接口,是否能够组合在一起;

然后我们用这两个函数去定义我们之前例子中的两个函数:

const mod2 = mod(2);
const eq1 = eq(1);

const isOdd = (x) => eq1(mod2(x));

我们先给mod和eq两个函数传了一个参数,让它们从一个一般化的函数成为一个特殊化的函数,之后isOdd函数只接受一个参数,这个参数直接传给mod2,然后它的返回值再直接传给eq1。这样的定义其实已经从命令式慢慢过渡到声明式了,只不过还不能算是函数式的Point-free。不过这样直接传一个参数,计算后直接将返回值传给下一个函数作为入参,这在数学中有一个名字叫做Composition。是的没错,就是文章开头的那个composition,其实compose函数也很简单,他只做了一件事:

const compose = (fn2, fn1) => {
    return function composed(v) {
        return fn2(fn1(v));
    }
}

所以我们的定义又会变成:

const isOdd = compose(eq1, mod2);

但是其实在函数式编程中一般情况下大家都不会去特意构造一个特殊的函数(mod2, eq1),所以其实最后的Point-free定义就是:

const isOdd = compose(eq(1), mod(2));

其实乍一看上去,compose的参数也是反直觉的,人们通常习惯从左到右进行阅读,但其实这里的运行顺序是从右到左的。这当然是有原因的:

  1. 我们回到之前提到的eqmod函数中的第一个重要特点 — 参数顺序,我们假如他们的参数顺序是符合人类直觉的,那么他们就不可能像我们最终定义哪样进行定义;
  2. 当然你也可以说,我可以改变compose函数的定义来让他们可以组合在一起呀。这就是我第二个原因,在数学中函数复合写作f o g(相当于compose(f, g)),意思是一个函数接收一个参数x,并返回一个f(g(x))。

所以其实compose的定义是严格遵循数学中的composition的,当然如果你是在觉得这有点反直觉,其实也有一个叫做pipe的函数,作用跟compose完全一样,只是参数的顺序完全相反。

而且compose也是完全符合数学中的结合律的:

const associative = compose(f, compose(g, h)) === compose(compose(f , g), h); // true

这也就意味着我们可以有更高的灵活性去组合我们的函数。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,783评论 5 472
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,396评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,834评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,036评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,035评论 5 362
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,242评论 1 278
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,727评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,376评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,508评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,415评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,463评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,140评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,734评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,809评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,028评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,521评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,119评论 2 341

推荐阅读更多精彩内容