函数式编程&响应式编程学习

Functional And Reactive Programming Learning

Posted by Elliot on July 1, 2017

版权声明:本文为博主原创文章,未经博主允许不得转载;如需转载,请保持原文链接。

函数式编程&响应式编程学习

最近在看《重构,改变现有代码设计》一书,其中提到提炼函数,于是想到了函数式编程,发现自己对于函数式编程还是停留在很浅的层面,所以打算写一篇讲讲函数式,本文只做自己的学习笔记;

编程范式

函数式编程是一种编程范式,那么首先什么是编程范式?维基百科:编程范型或编程范式(英语:Programming paradigm),(范即模范之意,范式即模式、方法),是一类典型的编程风格,是指从事软件工程的一类典型的风格(可以对照方法学)。如:函数式编程、程序编程、面向对象编程、指令式编程等等为不同的编程范型。

函数式的历史

函数式编程中最古老的例子莫过于1958年被创造出来的LISP了。用LISP编程可以达到精简人力的目的。函数式编程更加现代一些的例子包括scheme、Haskell、Clean、Erlang和Miranda等。虽然λ演算并非设计来于计算机上运行,但它可以被视作第一个函数式编程语言。1980年代末期,集函数式编程研究成果于大成的Haskell发布。

函数式编程

函数式编程(英语:functional programming)或称函数程序设计,又称泛函编程,是一种编程范型,它将电脑运算视为数学上的函数计算,并且避免使用程序状态以及易变对象。函数编程语言最重要的基础是λ演算(lambda calculus)。而且λ演算的函数可以接受函数当作输入(引数)和输出(传出值)。 比起命令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。

以上所述来自维基百科;

上面一大堆理论,看的有点晕;我们来看一个例子,这样更容易理解函数式编程,现在有一个数学表达式:

(1 + 2) * 3 - 4

传统的过程式编程,可能这样写:

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

函数式编程要求使用函数,我们可以把运算过程定义为不同的函数,然后写成下面这样:

result = subtract(multiply(add(1,2), 3), 4);

函数式编程的特性

1. 函数是”第一等公民”

所谓”第一等公民”(first class),指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。

2. 只用”表达式”,不用”语句”

“表达式”(expression)是一个单纯的运算过程,总是有返回值;”语句”(statement)是执行某种操作,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。 原因是函数式编程的开发动机,一开始就是为了处理运算(computation),不考虑系统的读写(I/O)。”语句”属于对系统的读写操作,所以就被排斥在外。 当然,实际应用中,不做I/O是不可能的。因此,编程过程中,函数式编程只要求把I/O限制到最小,不要有不必要的读写行为,保持计算过程的单纯性。

3. 没有”副作用”

所谓”副作用”(side effect),指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。 函数式编程强调没有”副作用”,意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。

4. 不修改状态

上一点已经提到,函数式编程只是返回新的值,不修改系统变量。因此,不修改变量,也是它的一个重要特点。 在其他类型的语言中,变量往往用来保存”状态”(state)。不修改变量,意味着状态不能保存在变量中。函数式编程使用参数保存状态,最好的例子就是递归。下面的代码是一个将字符串逆序排列的函数,它演示了不同的参数如何决定了运算所处的”状态”。

function reverse(string) {
  if(string.length == 0) {
    return string;
  } else {
    return reverse(string.substring(1, string.length)) + string.substring(0, 1);
  }
}

由于使用了递归,函数式语言的运行速度比较慢,这是它长期不能在业界推广的主要原因。

5. 引用透明

引用透明(Referential transparency),指的是函数的运行不依赖于外部变量或”状态”,只依赖于输入的参数,任何时候只要参数相同,引用函数所得到的返回值总是相同的。 有了前面的第三点和第四点,这点是很显然的。其他类型的语言,函数的返回值往往与系统状态有关,不同的状态之下,返回值是不一样的。这就叫”引用不透明”,很不利于观察和理解程序的行为。

意义

1. 代码简洁,开发快速

函数式编程大量使用函数,减少了代码的重复,因此程序比较短,开发速度较快。

2. 接近自然语言,易于理解

函数式编程的自由度很高,可以写出很接近自然语言的代码。

3. 更方便的代码管理

函数式编程不依赖、也不会改变外界的状态,只要给定输入参数,返回的结果必定相同。因此,每一个函数都可以被看做独立单元,很有利于进行单元测试(unit testing)和除错(debugging),以及模块化组合。

4. 易于”并发编程”

函数式编程不需要考虑”死锁”(deadlock),因为它不修改变量,所以根本不存在”锁”线程的问题。不必担心一个线程的数据,被另一个线程修改,所以可以很放心地把工作分摊到多个线程,部署”并发编程”(concurrency)。

5. 代码的热升级

函数式编程没有副作用,只要保证接口不变,内部实现是外部无关的。所以,可以在运行状态下直接升级代码,不需要重启,也不需要停机。Erlang语言早就证明了这一点,它是瑞典爱立信公司为了管理电话系统而开发的,电话系统的升级当然是不能停机的。

以上所述来自大神阮一峰的博客函数式编程初探

速度和空间上的顾虑

函数式编程常被认为严重耗费CPU和内存资源。主因有二:

  • 在实现早期的函数式编程语言时并没有考虑过效率问题。
  • 有些非函数式编程语言为求提升速度,不提供自动边界检查或自动垃圾回收等功能。

回到开头,对于非函数式编程来说,函数式编程非常有借鉴意义,对于代码重构,模块解耦有非常大的帮助。本文仅用作本人学习之用,学习函数式的一些概念,如有不当之处,还请指点;

函数编程之尾递归

尾调用(Tail Call)是函数式编程的一个重要概念,本文介绍它的含义和用法。

现有答案理解起来都好复杂,我答个简单易懂的。尾递归就是操作的最后一步是调用自身的递归。 尾调用不一定出现在函数尾部,只要是最后一步操作即可。

这是尾递归:

function f(x) {
   if (x === 1) return 1;
   return f(x-1);
}

(这个程序没什么意义,仅作为理解辅助之用)。

这不是尾递归:

function f(x) {
   if (x === 1) return 1;
   return 1 + f(x-1);
}

后者不是尾递归,是因为该函数的最后一步操作是用1加上f(x-1)的返回结果,因此,最后一步操作不是调用自身。注意,后面这段代码的递归也发生在函数末尾,但它不是尾递归。尾递归的判断标准是函数运行最后一步是否调用自身,而不是是否在函数的最后一行调用自身。因此阶乘n!的递归求法,尽管看起来递归发生在函数末尾,其实也不是尾递归:

function factorial(n) {
   if (n === 1) return 1;
   return n * factorial(n-1); // 最后一步不是调用自身,因此不是尾递归
}

使用尾递归可以带来一个好处:因为进入最后一步后不再需要参考外层函数(caller)的信息,因此没必要保存外层函数的stack,递归需要用的stack只有目前这层函数的,因此避免了栈溢出风险。一些语言提供了尾递归优化,当识别出使用了尾递归时,会相应地只保留当前层函数的stack,节省内存。

所以,在没有尾递归优化的语言,如java, python中,鼓励用迭代iteration来改写尾递归;在有尾递归优化的语言如Erlang中,鼓励用尾递归来改写其他形式的递归。

函数编程之柯里化

函数式编程有一个概念,叫做柯里化(currying),意思是将多参数的函数转换成单参数的形式。

function factorial(n, total = 1) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}
factorial(5) // 120

响应式编程

在计算机中,响应式编程是一种面向数据流和变化传播的编程范式。数据更新是相关联的。这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。

命令式编程就是通过表达式或语句来改变状态量,例如c = a + b就是一个表达式,它创建了一个名称为c的状态量,其值为a与b的加和。下面的a = 5是另一个语句,它改变了a的值,但这时c是没有变化的。所以命令式编程中c = a + b只是一个瞬时的过程,而不是一个关系描述。在传统的开发中,想让c跟随a和b的变化而变化是比较困难的。而让c的值实时等于a与b的加和的编程方式就是响应式编程。

把函数范式里的一套思路和响应式编程合起来就是函数响应式编程。

在MVC软件架构中,响应式编程允许将相关模型的变化自动反映到视图上,反之亦然。

参考

函数式编程初探 维基百科 iOS与函数式编程