×

函数式编程-学习笔记:又为这个复杂的世界增加了一个熵(shang)

96
zhangyin
2017.02.09 17:28* 字数 2592

函数式编程(FP) 学习笔记

  • 同大家分享、交流学习成果,观点不一定正确,请大家用辩证的眼光来看待本文内容
  • 鸟瞰函数式编程的概况
  • 简单了解一下函数式编程中的基本脉络和一些主要思想
  • 看看大家怎么说

1.

"函数式编程" 是一种 "编程范式"(programming paradigm)

也就是如何编写程序的方法论

In computer science, functional programming is a programming paradigm that treats computation as the evaluation of mathematical functions and avoids state and mutable data.

函数式编程是一种编程模型,他将计算机运算看做是数学中函数的计算,并且避免了状态以及变量的概念

FP属于"结构化编程"的一种,主要思想是把运算过程尽量写成一系列嵌套的函数调用;
模块化是成功编程的关键,而函数编程可以极大地改进模块化。
f00.jpg

f1.jpg

f2.jpg

f3.jpg

f4.jpg

f5.jpg

f6.jpg

f7.jpg

f8.jpg

f9.png

2.

函数式编程的特性

  • 闭包和高阶函数
  • 惰性计算
  • 递归

闭包

闭包是自包含的函数代码块,可以在代码中被传递和使用。(自包含:组件不依赖其他组件,能够以独立的方式供外部使用。)

闭包就是引用了自由变量的函数。

这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。

如何来理解这个自由变量呢?

自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量

什么样的变量是自由变量呢?

如下片断中的freeVar就是个自由变量:

function wrapper() {
  var freeVar = 42;
    function inner() {
    return 2 * freeVar;
}
return inner;

自由变量在闭包生成之前,并不是函数的一部分。在函数被调用时,闭包才会形成,函数将这个自由变量纳入自己的作用域,也就是说,自由变量从此与定义它的容器无关,以函数被调用那一刻为时间点,成为函数Context中的成员。

# 例1
//定义一个求和闭包
//闭包类型:(Int,Int)->(Int)
var c = 100
let add:(Int,Int)->(Int) = {
    (a,b) in
    return a + b + c
}
//执行闭包
let result = add(1,2)

//打印闭包返回值
print("result=\(result)")

----
# 例2
//包含一个自由变量的闭包
func makeIncrementor(forIncrement amount:Int)->()->Int{
    var runningTotal = 0//这是一个自由变量
    func incrementor()->Int{
        runningTotal += amount
        return runningTotal
    }
    return incrementor
}

let incrementByTen = makeIncrementor(forIncrement:10)

incrementByTen()//10
incrementByTen()//20
incrementByTen()//30

高阶函数

在 Wikipedia 中,是这么定义高阶函数(higher-order function)的,如果一个函数:

  • 接受一个或多个函数当作参数
  • 或者,把一个函数当作返回值

那么这个函数就被称作高阶函数。

import UIKit
var usernames = ["Lves", "Wildcat", "Cc", "Lecoding"]
func backWards(s1: String, s2: String) -> Bool
{
    return s1 > s2
}

var resultName1 = usernames.sorted(by:backWards)

//结果:resultName1: ["Wildcat", "Lves", "Lecoding", "Cc"]

惰性计算

惰性计算(Lazy Evaluation),目标:最小化计算机要做的工作。

两层含义:“延迟计算”和“短路求值”

在使用延迟求值的时候,表达式不在它被绑定到变量之后就立即求值,而是在该值被取用的时候求值。
例如:
x:=expression; (把一个表达式的结果赋值给一个变量x)
从字面意义上来看,调用这个表达式,将立即把被计算结果放置到 x 中,
但是在惰性计算中,则不会先立即进行计算,而是直到在后续表达式中对 x 的引用有取值的需求时才进行计算。而后续表达式自身的求值也可以被延迟,最终为了生成让外界看到的某个符号而计算这个快速增长的依赖树。

来看一个困惑前端的示例,循环添加事件:

<meta charset="UTF-8">
<button>第1条记录</button>
<button>第2条记录</button>
<button>第3条记录</button>
<button>第4条记录</button>
<button>第5条记录</button>
<button>第6条记录</button>

<script type="text/javascript">

//test 1 没有使用闭包
var buttonst_obj = document.getElementsByTagName("button");
for (var i = 0, len = buttonst_obj.length; i < len; i++) {
   buttonst_obj[i].onclick = function() {
      alert(i);
   };
}
 
 上述片断的结果是:每个Button弹出的都是6,因为没有形成有效的闭包。
 
 闭包是有延迟求值特性的,所以在函数得到执行时,i === 6。(循环执行完成后,onclick事件才触发,此时i已经=6了)
 
 如果我们将它改成下面这样,让 i 作为外层函数的参数而被内层函数闭包,结果则是我们想要的:

//test 2 使用闭包
var buttonst_obj = document.getElementsByTagName("button");
for (var i = 0, len = buttonst_obj.length; i < len; i++) {
     buttonst_obj[i].onclick = clickEvent(i);
}

function clickEvent(i){
   return function () {
      alert(i);
   }
}

</script>
 
 
 Why? 
 
 因为这个clickEvent(i) 高阶函数,
 
 它将 i 作为自由变量传递(注意:i 并不是内函数的参数,也不是内函数的一部分),
 
 在 click 时闭包已经形成并被传递。
 
 

短路求值

作为"&&"和"||"操作符的操作数表达式,这些表达式在进行求值时,只要最终的结果已经可以确定是真或假,求值过程便告终止,这称之为短路求值(short-circuit evaluation)。这是这两个操作符的一个重要属性。


递归 (略)


3.

函数式编程还具有以下五个鲜明的特点

  • 函数是"第一等公民"
  • 只用"表达式",不用"语句"
  • 没有"副作用"
  • 不修改状态
  • 引用透明性

函数是"第一等公民"

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

举个栗子:下面代码中的cprint函数,可以作为另一个函数的参数。

func cprint(i:Int){ 
  print(i)
}
let arr = [1, 2, 4]
arr.forEach(cprint)

只用"表达式",不用"语句"

"表达式"(expression)是一个单纯的运算过程,总是有返回值;

"语句"(statement)是执行某种操作,没有返回值。

函数式编程要求,只使用表达式,不使用语句。

也就是说,每一步都是单纯的运算,而且都有返回值。

函数式编程的动机,一开始就是为了处理运算(computation),不考虑系统的读写(I/O)。

"语句"属于对系统的读写操作,所以就被排斥在外。

当然,实际应用中,不做I/O是不可能的。

因此,编程过程中,函数式编程只要求把I/O限制到最小,不要有不必要的读写行为,保持计算过程的单纯性。


没有"副作用"

所谓"副作用"(side effect),指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。

函数式编程强调没有"副作用",意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。


不修改状态

变量往往用来保存"状态"(state)。

不修改变量,意味着状态不能保存在变量中。

函数式编程使用参数保存状态,最好的例子就是递归。

举个栗子:计算阶乘 n!

//使用循环的方式来计算阶乘,total变量用于保存临时状态
func cJieChen(n:Int) -> Int {
    var total = 1
    for i in 1...n{
        total = total * i
    }
    return total
}

var t = cJieChen(n:4)

//使用递归方式计算阶乘,用参数来传递状态
func jiechen(n:Int) -> Int {
    if(n==0){
        return 1
    }else{
        return n * jiechen(n:n-1)
    }
}

var a = jiechen(n: 4)

引用透明性

函数式编程还能够增强引用透明性,

即如果提供同样的输入,那么函数总是返回同样的结果。

也就是说,

表达式的值不依赖于可以改变的值的全局状态。

这使您可以从形式上推断程序行为,

因为表达式的意义只取决于其子表达式而不是计算顺序或者其他表达式的副作用。

这有助于验证正确性、简化算法,甚至有助于找出优化它的方法。


副作用

副作用指的是修改系统状态的语言结构。

因为 FP 语言不包含任何赋值语句,变量值一旦被指派就永远不会改变。而且,调用函数只会计算出结果 ── 不会出现其他效果。因此,FP 语言没有副作用。


4.

函数式编程好处

  • 代码简洁,开发快速
  • 接近自然语言,易于理解
  • 更方便的代码管理
  • 易于"并发编程"
  • 代码的热升级

代码简洁,开发快速

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

接近自然语言,易于理解

函数式编程的自由度很高,可以写出很接近自然语言的代码。
例如:mansory中的链式调用;

更方便的代码管理

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

易于"并发编程"

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

多核CPU是将来的潮流,所以函数式编程的这个特性非常重要。

代码的热升级

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


5.

顾虑

函数式编程常被认为严重耗费在CPU和存储器资源

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

6.

柯里化(Currying)

柯里化(Currying),又称部分求值(Partial Evaluation),是一种函数式编程思想,就是把接受多个参数的函数转换成接收一个单一参数(最初函数的第一个参数)的函数,并且返回一个接受余下参数的新函数技术。

currying.jpg

Currying wiki

例3

//利用闭包进行柯里化

//假设原先有一个下面这样的函数
func addOrignal(_ a1:Int,_ a2:Int) -> Int {
    return a1+a2
}
let a1 = addOrignal(2,3)
print("a1=\(a1)")


//使用柯里化的方式进行改造
/**
 这个栗子中的4行代码,讲述的大概是下面的故事:
 (1)函数addTo的返回值为 (Int) -> Int :记作R,R仍然是一个函数;
 (2)函数addTo接收一个数字类型的参数(adder);
 (3)返回函数(R)接收一个数字类型的参数(num);
 (4)返回函数(R)的返回值是一个数值,这个数值为:num+adder
 (5)adder是函数addTo的参数,而num是函数R的参数,当函数R被创建后,参数adder被闭包到函数R当中,并参与函数R的运算;
*/
func addTo(_ adder:Int) -> (Int) -> Int {
    return {
        num in
        return num + adder
    }
}



let add2 = addTo(2)
let a2 = add2(3)
print("a2=\(a2)");
//a2=4

let add3 = addTo(3)
let a3 = add3(3)
print("a3=\(a3)");
//a3=6


Map & Reduce

MapReduce是一种编程模型,用于大规模数据集(大于1TB)的并行运算。概念"Map(映射)"和"Reduce(归约)",是它们的主要思想,都是从函数式编程语言里借来的,还有从矢量编程语言里借来的特性。它极大地方便了编程人员在不会分布式并行编程的情况下,将自己的程序运行在分布式系统上。 当前的软件实现是指定一个Map(映射)函数,用来把一组键值对映射成一组新的键值对,指定并发的Reduce(归约)函数,用来保证所有映射的键值对中的每一个共享相同的键组。

pipeline

这个技术的意思是,把函数实例成一个一个的action,然后,把一组action放到一个数组或是列表中,然后把数据传给这个action list,数据就像一个pipeline一样顺序地被各个函数所操作,最终得到我们想要的结果。


7.

Lambda表达式

“Lambda 表达式”(lambda expression)是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的lambda抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。Lambda表达式可以表示闭包(注意和数学传统意义上的不同)。

var resultName2 = usernames.sorted() {
    (s1: String, s2: String) -> Bool in
    return s1 < s2
}
sorted方法使用匿名闭包函数

8.

反面的观点

不论是面向对象编程还是函数式编程,如果你走了极端,那都是错误的。
面向对象编程的极端是一切都是对象(纯面向对象)。
函数式编程的极端是纯函数式编程语言。

为什么说面向对象编程和函数式编程都有问题(30)

反人类
难找工作
好多语言设计得不real world,用来解决实际问题很无(sha)力(bi)
学好函数式编程是要数学基础的,尤其是了解数学是如何从更高层面抽象已经被抽象过的东西。
学的人容易感到智商不够用
union-find算法的Dr. Harrop说:目前我们还没有发现一个有效率的纯函数的union-find集合。
也就是说:对于有状态的操作命令式操作会比声明式操作更有效率。

纯函数式编程的缺点

april.jpg
w.jpg

参考资料

(1)Functional programming

(2)百度百科

(3)闭包1 闭包2

(4)Lambda calculus

(5)Currying

随笔乱记
Web note ad 1