vlambda博客
学习文章列表

当我们在谈函数式编程(Functional Programming,FP),到底在谈论什么?

最早知道函数式这个概念是在知乎上,看到有人讨论垠神和田春,有个知乎评论说写 lisp 的都容易走火入魔,当时就去搜了下 lisp 这门语言, 大部分介绍都还是正面为主,学术派、函数式这些都是它的标签。


后来转行当了程序员,做前端那会在学习 React 的时候很多介绍文章都说 React 是很 FP 的,再到后面陆陆续续知道了 Haskell、 Erlang、 Scala 这些函数式语言,原来工业界还是有函数式的一席之地。(以前看到一个言论是说 这个世界是基于 OOP (面向对象编程)的,一直深信不疑。

入门

说实话,FP 的文章是很晦涩难懂的,是非常反直觉的。有很多讲 FP 入门的文章都写得很不错,但是有很多都夹杂着大量的术语和数学公式。我觉得入门还是要简单直接,FP 的本身概念是不难懂的。


那到底什么是函数式编程?closure (闭包)、currying柯里化)这些经常在红宝书(一本很经典的讲 Javascript 的语法书)出现的词汇到底和函数式是什么样的关系?

Lambda

Lambda 是 FP 编程绕不过去的一个核心概念,那什么是 Lambda?函数的参数是函数,返回值也是函数。最初的时候这种函数用希腊字母lambda(λ)表示,因此得名。


所以以后听到 Lambda 的时候就可以直接把他等价于函数这个单词就行。


所以现在绝大部分语言都实现了函数是第一等公民(First Class),即函数跟其他数据类型是平等地位,比如 Golang 里,你可以把函数赋值给一个变量,把函数当做参数传递给另一个函数。这是实现函数式编程很多概念的基础,比如闭包、柯里化等等。如下:

bar := 1foo := func (a, b int) int { return a + b}

有一说一,Java 8 才支持 Lambda 表达式,之前都没支持,前几天看他们 On Java8 中文版里面函数式章节的一段话,看的还是闻着伤心,听者流泪。

Currying


柯里化是函数式编程最常用的运算手段,它表达的概念很简单:在一个 FP 语言中函数(而不是类)被作为参数进行传递,从而用于减少函数参数的数量。

举个例子,我们实现一个简单的两数想加的函数 add

func add(x, y int) int { return x + y}
func main() { fmt.Println(add(1, 2))}

柯里化之后就变成这样

func add(x int) func(int) int { return func(y int) int { return x + y }}
func main() { fmt.Println(add(1)(2)) //3 //or bar := add(1) fmt.Println(bar(2)) //3}

是不是很像设计模式的适配器模式,在函数式编程里为什么没有所谓的设计模式这种概念,就是因为抽象程度已经很高,纯函数式语言像 Lisp 它们这种表达能力很强,所以就不需要所谓的设计模式。


so 函数式就是这么直观,什么时候使用柯里化呢?很简单,当你想要封装函数的时候,就是用柯里化的时候。

compose

我们现在了解了柯里化,为什么要有柯里化这个方法呢?这就要说到函数式另一个最基本的特性:组合。


什么是组合呢?如下:

func bar(x int) int { return x * 2}
func foo(x int) int { return x + 1}
compose := func(bar, foo func(int) int) func(int) int { return func(x int) int { return bar(foo(x)) } }
fmt.Println(compose(bar, foo)(2)) //6

可以看到,如果一个值要经常多次函数运算,才能变成另一个值,在函数式编程里就可以把中间步骤合并在一起,这就是组合。


可以看到,要合成 bar(foo(x)) 这种前提就是 bar 和 foo 这种函数只能只能接受一个参数,如果接受多个参数,合成就有点麻烦,所以函数式编程函数默认一般都是一个参数。


组合像一系列管道那样把不同的函数联系在一起,数据就可以也必须在其中流动。

总结

当然,软件行业有一句著名的话叫:没有银弹。


函数式语言和函数式编程是银弹吗?它的缺点也显而易见,比如没有 for 循环,只有递归。因为 for 循环是引入了新的变量的,这是在函数式编程里不能接受的。所以带来的问题就是递归次数多了会引起栈溢出。性能会有很大的问题,不过现在编译器都实现了尾递归程序转换为循环,这样内存会大大减少。但是 Golang 是没有实现尾递归优化的。所以在 Golang 程序里尽量减少递归的使用。


纯粹追求函数式会自缚手脚,比如传入指针这种操作是函数式不允许的,这样会让函数变得不纯。想想我们日常开发中是不是有很多传入指针的操作?这个世界是有副作用的,IO 操作这些 (比如键盘的输入输出) 是不可避免的,这些都是函数式所视为洪水猛兽的。


所以我的观点还是,面向对象和函数式编程我全都要(.jpg)。在编写软件过程中,尽量让要测试的函数变得纯,这样有利于我们单元测试。在建模过程中,如果要对一组数据进行排序、加工、查询的话就可以利用函数式进行建模,这样就很自然。对具体问题还是要分析下,到底是面向对象合适一点,还是函数式编程合适一点。


其实函数式编程深入进去会发现很反直觉,它其实是比面向对象,面向过程难很多的。这篇文章对函数式很多概念其实都没有介绍到,比如函数式的起源是数学的一个分支范畴论(Category Theory)。而理解函数式编程的关键就是理解范畴论,这是一门很复杂的数学。像函数式编程里的数据类型是被成为函子的,这是一种范畴。处理 IO 这些副作用的就是 Monad 函子,它最重要的作用就是实现了 IO 的输入输出。还有函数式编程的好姐妹响应式编程,这里就不再进行扩展。大家感兴趣可以去延伸阅读里扩展阅读下。


本文章来源网络,若有侵权,请联系我,立删。