zhuanyongxigua / blog

blog
77 stars 9 forks source link

JavaScript函数式编程之为什么要函数式编程(非严谨技术层面的扯淡) #20

Open zhuanyongxigua opened 6 years ago

zhuanyongxigua commented 6 years ago

我的github https://github.com/zhuanyongxigua/blog

这可能是一篇会被经常改动的文章,它记录了现在的我对函数式编程粗浅的理解。

函数式编程并不是github上面的一个工具库,它的年龄比JavaScript要大得多,它是一种经过了几十年,被众多计算机科学家证明了的行之有效的编程范式。它不是学会了几个函数式编程工具的API就能完全掌握的,它更多的是编程思维上的转变。具体一点的代码实例可以看《JavaScript函数式编程之pointfree与声明式编程》

那到底为什么用使用函数式编程

如果总结成一个字,那就是为了“爽”。“爽”在哪?读和写

这可能会让你不以为然,读和写对你来说可能不是最重要的事情。不过以我当前的认知水平来看,设计模式、编程范式,甚至是编程语言本身,他们的最大意义都在于这两个字。

代码是给人看的

比如编程语言,它的落点在于“语言”,“语言”是用来干什么的?交流沟通。跟谁交流沟通?跟人,而非机器。我们写的代码会经过层层编译,最终转化成0和1才能被计算机执行。那我们为什么要这么麻烦,而不直接用0和1编程?因为不好写也不好读。如果你手速足够快,大脑也足够发达,用0和1编程也是可以的。可那样的话,还需要计算机吗?虽然层层编译的过程,势必会降低效率,不过有摩尔定律来接盘,大可不必担心。

函数式的“好读”体现在哪里?

如果全部功能的实现只需要几行代码,大谈设计模式、编程范式就太矫情了。我们之所以需要这些东西,是因为随着代码量的增多,代码的读写对我们造成了困扰。

那函数式编程是如何解决这些问题的?

比如纯函数,它有很多优点,个人认为最重要的是:它明显的降低我们的认知负荷。

从最普通的函数说起:

var a = 1;
function A(val) {
  a = 2;
  return a + val;
} 

这里的变量a就是被大家嗤之以鼻的全局变量。它非常容易导致出现bug,当代码量很多的时候,很可能会因为重名而被覆盖,从而导致其他使用了这个变量的程序出错。即便是正常的修改,当我们再次使用函数A的时候,弄清变量a当前的状态也是十分的困难,这就使得函数A变得难以理解,难以理解的函数就得不到使用者的信任

面向对象编程的“封装”在一定程度上解决了这个问题:

var obj = {
  a: 1,
  A: function(val) {
    return this.a + val;
  }
}

封装了之后全局变量大大的减少了,即便有两个对象重名了,相对上面的情况,修改起来也不会有那么大的负担。但它并没有彻底解决这一问题,因为属性a是可变的,每次调用方法A的时候,弄清属性a的状态依然不容易。

而函数式编程不存在这个问题。在函数式编程中,使用可变的外部的变量是不被允许的。例如:

var a = 1;  // 除了当做参数传入,使用变量a的函数都不是纯函数
function A(val) {   // 不是纯函数
  return a + val;
}

const b = 1;
const B = function(val) {   // 纯函数
  return b + val;
}

随着代码量逐渐增多,这种写法的优势就愈发明显了,如果还能更进一步的减少耦合,那你就可以在很大程度上去“断章取义”,不去管上下文。每一个纯函数我们都可以充分信任,它的输出只与传进去的参数有关。

不仅仅在函数式编程中,面向对象的编程通常也会建议写纯函数。

纯函数对代码的调试也有极大的好处,这也是函数式编程的优点之一:方便调试。

在初学函数式编程的时候,会很难理解这一点,因为当我们使用纯函数帮我们解决问题的时候,总是得不到想要的结果,尤其是在使用函数式的工具库进行函数组合的时候,断点调试都很困难,就算你把断点打到了第三方的工具库里面,你真的确定你能在短时间内看懂人家的源码吗?这个时候往往只能想办法去console.log。不过这个问题是可以通过不断的学习总结来解决的。即便使用其他的方式编程,不熟悉所用的工具也会出现同样的情况,这并不是函数式编程的问题。

绝大多数的bug都是由副作用引起的。也就是系统状态的变化。这一点无法通过学习来解决,你能记住成千上万个状态,并能时刻了解他们的变化吗?可如果我们是在函数式编程,使用了大量的纯函数,由于纯函数不产生副作用,在调试的时候,我们只需要去调试会产生副作用的部分就可以了。

在函数式编程中,一切的技巧的目的都是为了让你的代码更加的函数式。可掌握有些技巧的成本很高,这就导致部分人对函数式编程的”可读性“产生了质疑。

其实,函数式代码的可读性与读代码的人的函数式编程的能力有直接的关系。如下图:

命令式编程很符合人类的思维习惯,入门的成本很低,而声明式的函数式编程的学习则有一定的门槛,对于新手来说,可能几乎没有可读性。只有不断的深入学习,这种可读性的优势才会逐渐的体现出来。这可能是导致函数式编程没有被广泛应用的最主要原因。

函数式编程的一些问题

  1. 可读性的疑惑。pointfree的声明式的写法有时候很难讲到底是提高了可读性还是降低了可读性,特别是在你没有在函数的命名上很好的体现出这个函数的功能、参数和返回值的详细信息的情况下(能提现出来吗?)。真正的函数式编程语言有类型签名,当然你可以用这种方式给你的函数加上注释。可是,熟练掌握类型签名也不是一件容易的事情。
  2. 用的人太少。可能是入门门槛的问题,现在大多数的编程还是面向对象的。如果仅仅是用一个函数式的工具库帮助处理一下数据可能不是大问题。可如果你把各种进阶的技巧如函子、范畴学的公式等在实际项目中用的不亦乐乎,你的不太熟悉函数式编程的同事可能会骂街。
  3. 函数式编程通常会大量的使用柯里化,理论上会影响性能,进而影响体验。可由于运行环境种类太多,这一点不好量化,要根据具体的情况讨论。
  4. 习惯了命令式编程,转变成函数式编程会有点困难。你的面向对象用的越好,可能转起来就越困难。我(我面向对象用的不好)目前在用函数式编程时,很多时候都是先用命令式的方式想清楚,再改成函数式。在脑袋里面装一个编译器,十分费电。

如果还要举的话。。。

  1. 代码量很大的时候,声明式的编程,给函数起名字会很辛苦。。。。。。。吧?

函数式编程与面向对象编程

面向对象编程更倾向于封装,而函数式编程更倾向于抽象。

面向对象编程抽象的结果是一个类,函数式编程抽象的结果是一个过程(一个函数)。

封装和抽象有什么区别?

他们并没有明显的界限,抽象是通过封装来实现的,我找不到一个只是抽象而不是封装的例子。封装和抽象的目的略有不同。简单的讲,封装只是把一些会再次使用的操作包起来,等待下次使用。抽象也会有“包起来”的动作,可它更多的目的在于要划清界限。这就好比让我们解释一个概念,如果你说“我懂,但我就是说不出来”,那么大概率你是没有懂。我们是否能清晰准确的说清楚,取决于我们脑中对这一概念的界限是否划清楚了。如果没有划清楚,我们就没办法下断语去说明它是什么和它不是什么。抽象也是如此,它需要我们搞清楚它是干什么的,把不应该干的事情都去掉。“它是什么”这一点越明确,那么这一封装的抽象程度就越高。

参考资料: