函数式初识(2) - 纯函数

by Teobler on 21 / 08 / 2019

views

Function VS Procedures

这里的纯函数指的是在函数式编程里面的纯函数。要理解好纯函数这个概念,就需要理解好程序和函数的区别。

程序(Procedure)指的是一系列的逻辑运算,也就是大家所熟知的循环,分支,跳转等,然后利用这些运算在你的大程序(program)中做一些事情。看起来是不是很像函数?那么什么才是函数?

Function

  1. 一个函数不但应该有输入(just one is the best),还应该有输出(just one is the best),也就是说函数应该有return
  2. 函数只能去调用函数,如果一个函数调用了程序,那么这个函数也会变成程序
  3. 函数应该是一种输入输出之间的一种语义化的“关系”
    • 就像是数学中的函数一样,这也就意味着你不能不管输入是啥,单纯的返回一个随意的返回值
    • 所以在函数式编程中,函数的名字应该能够清晰的描述这个函数是一种怎样的关系,所以你就能够轻易的在你的程序中通过组合各个函数来得到一个新的函数
    • 需要明确的是,没有输入也是一种有效的输入;undefined也是一种有效的输出
  4. 在多次调用同一个函数时,相同的输入应该有相同的输出
  5. 最重要的一点,函数不应该有副作用,但是编程毕竟不是数学,这个要求在很多时候是很难达到的,所以退一步来说,我们应该尽最大可能降低函数的副作用。即使不能降低,那么我们有两个选择 — 首选我们当然是把副作用抽离函数,不行的话应该使副作用尽量明显,让读你代码的人能够一眼看出,这是这个函数的副作用。

Side Effects

广义上来说非直接的输入或输出都算是副作用,常见的副作用主要有:

  • 使用了外部变量做输入,或者是在计算后对位外部变量做了修改
  • I/O — console, 读/写文件
  • 数据库操作
  • 网络数据请求
  • DOM操作
  • 随机数
  • CPU请求

这也就说明了为什么副作用是不可避免的,只可能尽量降低。

那么为什么在函数中避免副作用是很重要的呢?首先函数式编程是基于数学理论的,如果你在写代码的时候能够严格遵循函数式编程的规范,将函数和副作用分开,那么如果你的程序出现了bug,你将能够比较容易的发现问题出在了哪里 — 如果是函数出了问题,那么一般情况下几乎可以断定是你用错了算法或逻辑(这里的“用错”指的是如同你在做数学题时用四则运算法则去做几何证明的问题),因为你根本不用去考虑是不是算法内部的问题,就如同你不用去证明1+1=2一样。但是更多的情况是你的副作用部分出了问题,因为你的副作用真的产生了你没有预知到的副作用。

而又说回JS,因为其实JS并不是一门严格意义上的函数式语言,所以对于JS来说其实最重要的部分是函数的调用,在JS中,当我们说纯函数时,其实更应该说的是纯的函数调用,因为就算你定义函数的时候“觉得”那已经是一个纯函数了,但是在真正调用的时候,依旧可能会产生一些你没有想到的副作用,比如下面的这个例子:

const getId = (obj) => {
    return obj.id;
}

第一眼看到这个函数,我问你,这是不是一个纯函数 — 是呀,当然是,完美的符合上面的所有条件。是的,从定义上来看的确是一个纯函数,那么如果我这样调用呢:

getId({
    get id() {
        return Math.random();
    },
});

很显然,这已经明显的破坏了上面所说到的纯函数的规则。所以在一个不是函数式的语言中,或者说是具体到JS来说,当你要去判断这是不是一个纯函数时,不但需要判断这个函数的定义,还需要去判断函数的调用。

所以什么算是一个纯的函数调用呢:可以用这个函数的返回值去替换这个函数的调用,即引用透明。

所以这到底有什么好处呢?其实最大的好处就是 — 对于代码阅读者来说,当他第一次看到某个函数时,他只需要记住那个结果,那么在整个项目中,只要看到这个函数被用相同的参数调用,就可以直接把他换成结果,无需再思考函数返回了什么。也就是说,他不用再浪费时间来思考函数里面发生了什么,可以将这部分精力用在更重要的地方。