函数式编程初探

函数式编程初探

让我们来看一下命令是编程和函数式编程的区别

ex:

1
1 + 2)* 3 - 4

过程式编程是这样:

1
2
3
var a = 1 + 2
var b = a * 3
var c = b - 4

函数式编程是这样的:

1
var result = sub(multi(3,add(1,2)),4)

函数式编程过程中,每个步骤都是单纯的表达式计算,都有返回值,和命令式语句say good bye,看起来是不是瞬间逼格提升了好多

因此函数式编程语言中,函数并不神秘,其唯一作用就是对表达是求值。而函数式编程本身也就是不断的表达式计算求值的过程。

好处都有啥

1.清楚直观,妈妈再也不用担心我为变量取名而绞尽脑汁了,贴近自然语言,易于维护,能够节省大量的代码量,上节的例子就很好的反映了这点

2.没有副作用
这点需要解释一下,简而言之,函数式编程致力于保持函数的纯净,不包含任何对外部引用的操作,没有副作用的纯净函数即是引用透明的,所有的依赖都来自函数入参。可以将其看成一个黑盒,仅关心输入输出值即可。利用这一点,函数式编程能够进行很好的单元测试。
纯净函数:

1
2
3
function add(a,b) {
return a + b
}

非纯净函数:

1
2
3
4
5
6
//用隐式的方式与外部进行数据交互
var a = 1
function add(a,b) {
a = 5
return a + b
}

3.immutable 不可变

程序员讨厌无法控制的东西,比如js中的对象。
ex:

1
2
3
4
var a = {name:"cassie"}
var b = a
b.name = "mike"
console.log(a) // {name:"mike"}

函数式编程提倡将程序中变化的东西最小化,使用immutable的数据。从根本上可以防止一些灵异事件的发生,减少需要追踪的记录。并且由于数据的不可变,可以视其为状态机,通过递进状态与回朔状态来保证程序逻辑可控。react就是这么玩的。
js中为了实践Immutable,也出现了immutable.js这类框架。

在不使用框架的前提下,结合es5,es6可以通过以下方式来保持immutable

  • 鼓励禁用var/let,所有东西都用const定义,强制immutable。但是注意const只能防止变量被赋值,引用对象的改变仍可以生效。
1
2
3
const a = [1,2,3]
a.push(4)
console.log(a) // [1,2,3,4]
  • 数组使用map,filter,reduce方法执行循环,使用concat代替push

  • 对象使用assign 来产生新对象而非直接编辑对象

4.引用透明

引用透明即如果提供同样的输入,那么函数总是返回同样的结果,概念和没有副作用差不多,他俩本身是息息相关的,但是却能够带来一个非常便利的特性。由于纯净函数不依赖于外部环境而只对传入参数进行表达式计算,那么我们就可以通过函数入参来保存函数运算过程中的状态,把所有状态整合到最后一次表达式计算中,也即尾调用。
以经典问题斐波那契数列为例。众所周知斐波那契数列的函数表达式为

1
f(n) = f(n-1) + f(n-2)

用递归写出来是这样的

1
2
3
function fib(n) {
return n < 2 ? n : fib(n-1) + fib(n-2)
}

虽然方法看上去非常的优雅,但是n较大时递归会拖长整个调用栈从而溢出。然而函数式编程中通过函数入参来保存变量状态的思路能够很好地解决这个问题。只需要将本次递归中需要保存的状态直接扔到内层递归中去,这样堆栈调用记录中只有一项,杜绝了内存溢出的可能。

1
2
3
function fib(n,a,b) {
return n < 2 ? a : fib(n-1,b,a+b)
}

函数Curry化

此处Curry并非是勇士当家球星Steve Curry,而是一个著名的美国数学家、逻辑学家Haskell Curry。为了纪念这位先哲,便将Curry化的概念以其命名。

为了介绍Curry化的概念,我们先来介绍一下高阶函数的概念。

高阶函数

何为高阶函数?先回想一个我们学过的数学知识:

y‘ = g(x) 对函数y=y(x)求导后其形式仍可表达为x的函数,代表一个函数到另一个函数的映射

高阶函数的概念可以说与其基本一致,其形式可以表达为

HOF: function => function

表示成返回函数的函数,用js 描述一下

1
2
3
4
5
function HOF() {
return doSomethind() {
...x
}
}

了解了高阶函数后,再回到Curry化的话题上来,此处我们不以特定语言为载体,仅讨论编程思想

先仍以add函数作为引子,以伪代码表示

1
add: (a,b) => a + b

add函数接受两个参数,返回两者相加的值。

那么如果我想add函数一次只接受一个变量,但仍然要完成add的功能怎么办,
其实只要把相加分步骤执行就可以了,表现为一个接受第二个参数并且返回两者相加函数的高阶函数

1
add: a => ( b => a+b )

因此函数Curry化可以看成是这样一个过程

1
curry: (a,b) => a + b -> a => b => a + b

那么问题又来了,Curry化的意义何在?在我刚开始学js时,学到Curry化这块觉得根本没什么卵用,但是现在被函数式编程思想有所启蒙以后发现这东西还是相当给力的。将多参的函数简化成一次仅处理一个参数的函数配合immutable可以很好的追踪变量来讨论代码中的问题,更好地去迎合单元测试。

当然并不是所有场景都使用Curry化的。
个人认为当函数中的参数必须成对出现才有意义时,是不适用Curry化的。

个人觉得Curry这种做法是为了更贴近元语言编程的习惯而制定的。