Tag Archives: 函数式编程

函数式编程术语解析

函数式编程蔚然成风,越来越多的开源项目、技术交流在使用函数式编程的术语降低开发或沟通成本,这无形中对不了解函数式编程的开发者造成了一定的学习门槛,翻译本文的初衷就是要普及函数式编程的基本知识,从新的角度扩展编程思维。至于为什么要使用 JavaScript 演示函数式编程,一方面是因为 JavaScript 的特性在很多方面与函数式编程浑然天成,另一方面是因为 JavaScript 是世界上最 XX 的语言……

Arity

指函数的参数数量,由 -ary 和 -ity 这两个英文后缀拼接而成:

const sum = (a, b) => a + b;const arity = sum.length;console.log(arity); // => 2

Higher-Order Functions

高阶函数,此类函数可以接收其他函数作为参数,也可以返回一个函数作为返回值:

const filter = (pred, xs) => {   const result = [];   for (let idx = 0; idx < xs.length; idx++) {       if (pred(xs[idx])) {            result.push(xs[idx]);        }    }   return result;};const is = (type) => (x) => Object(x) instanceof type;filter(is(Number), [0, ‘1’, 2, null]); // => [0, 2]

Partial Application

偏函数,在原函数的基础上预填充(pre-filling)部分参数并返回的新函数:

// 下面是一个创建偏函数的辅助函数const partial = (f, …args) => (…moreArgs) => f(…args, …moreArgs);const add3 = (a, b, c) => a + b + c;// 预填充 (add3, 2, 3) 三个参数,空置最后一个参数,返回一个新的函数const fivePlus = partial(add3, 2, 3); // (c) => 2 + 3 + cfivePlus(4); // => 9

JavaScript 中的 Function.prototype.bind() 函数是创建偏函数的最简单方式:

const add1More = add3.bind(null, 2, 3); // => (c) => 2 + 3 + c

Currying

柯里化,将一个接收多个参数的函数转化为单参数函数的方式,转化后的函数每次只接收一个参数,然后返回一个新函数,新函数可以继续接收参数,直到接收到所有的参数:

const sum = (a, b) => a + b;sum(2, 3)// => 6const curriedSum = (a) => (b) => a + b;curriedSum(40)(2) // => 42.const add2 = curriedSum(2); // (b) => 2 + badd2(10) // => 12

Function Composition

函数合成,接收多个函数作为参数并返回一个新函数的方式,新函数按照传入的参数顺序,从右往左依次执行,前一个函数的返回值是后一个函数的输入值:

const compose = (f, g) => (a) => f(g(a))const floorAndToString = compose((val) => val.toString(), Math.floor)floorAndToString(121.212121) // => “121”

Purity

一个纯函数需要满足两个条件,第一是函数的返回值只能由输入值(函数接收的参数)决定,也就是说纯函数接收相同的参数会返回相同的值;第二是纯函数不会对自身作用域之外的运行环境产生副作用(side effects),比如说不会改变外部环境中变量的值,这会被认为是不安全的行为:

let greeting;const greet = () => greeting = “Hi, ” + window.name;// greet() 执行时更改了外部环境的变量greet(); // => “Hi, Brianne”

纯函数示例:

const greet = (name) => “Hi, ” + name ;greet(“Brianne”) // => “Hi, Brianne”

Side effects

如果函数或表达式与其自身作用域之外的可变数据(mutable data)发生了读写操作,那么此时函数和表达式就产生了副作用:

let greeting;const greet = () => greeting = “Hi, ” + window.name;// greet() 执行时更改了外部环境的变量greet(); // => “Hi, Brianne”// new Date() 是可变数据const differentEveryTime = new Date();// 这里表示系统接收到的输入值是不确定的,是一种可变数据console.log(“IO is a side effect!”);

Idempotent

幂等,同一个函数使用相同的参数嵌套执行多次的结果与执行一次的结果相同:

$$f(…f(f(x))…)=f(x)$$

Math.abs(Math.abs(10))sort(sort(sort([2,1])))

Point-Free Style

point-free style 是一种不显式向函数传递参数的代码风格,通常需要柯里化和高阶函数来实现:

const map = (fn) => (list) => list.map(fn);const add = (a) => (b) => a + b;// Not points-free// numbers 是一个显式传递的参数const incrementAll = (numbers) => map(add(1))(numbers);// Points-free// add(1) 的返回值隐式传递给了 map,作为 map 的 list 参数const incrementAll2 = map(add(1));

point-free style 的函数看起来就像是一个赋值表达式,没有使用我们常见的 function 或 => 等来声明其接收的参数。

Predicate

断言,一个返回布尔值的函数:

const predicate = (a) => a > 2;[1, 2, 3, 4].filter(predicate); // => [3, 4]

Contracts

TODO

Guarded Functions

TODO

Categories

categories 内部都绑定了具体的函数用于约束或执行特定的逻辑,比如 Monoid。

Value

任何可以赋值给变量的值都可以称为 value:

5Object.freeze({name: ‘John’, age: 30}) // The `freeze` function enforces immutability.(a) => a[1]undefined

Constant

常量,初始化后不能再次执行赋值操作的数据类型:

const five = 5const john = { name: ‘John’, age: 30 }// 因为常量不可变,所以下面表达式一定为 truejohn.age + five === ({ name: ‘John’, age: 30 }).age + (5)

常量具有 referentially transparent 的特性,也就是说将程序中出现的常量替换为它们实际的值,并不会影响程序的结果。译者话外:实际上在 JavaScript 中的 const 所声明的常量并不是完全稳定的,使用 Immutable.js 演示更加恰当:

const five = fromJS(5);const john = fromJS({name: ‘John’, age: 30})john.get(‘age’) + five === ({ name: ‘John’, age: 30 }).age + (5)

f(g()) === g

Functor

functor 都拥有 map 函数,并且在执行 map 之后会返回一个新的 functor:

object.map(x => x) === objectobject.map(x => f(g(x))) === object.map(g).map(f)

JavaScript 中最常见的 functor 就是数组类型的实例:

[1, 2, 3].map(x => x); // => [1, 2, 3]const f = x => x + 1;const g = x => x * 2;[1, 2, 3].map(x => f(g(x))); // => [3, 5, 7][1, 2, 3].map(g).map(f);     // => [3, 5, 7]

Pointed Functor

pointed functor 都拥有 of 函数,用于接收和构建 functor。ES2015 提供了 Array.of 函数,所以数组实例就可以看成是 pointed functor:

Array.of(1) // => [1]

Lift

lift 发生在你将值放入 functor 的时候,如果你将函数 lift 进了 Applicative Functor,那么就可以使用这个函数处理传递给这个 functor 的值。某些 lift 的实现拥有 lift 或 liftA2 函数,便于在 functor 上执行相关的函数:

const mult = (a, b) => a * b;const liftedMult = lift(mult); // => this function now works on functors like arrayliftedMult([1, 2], [3]); // => [3, 6]lift((a, b) => a + b)([1, 2], [3, 4]); // => [4, 5, 5, 6]

lift 一个单参数的函数非常类似于 map 操作:

const increment = (x) => x + 1;lift(increment)([2]); // => [3][2].map(increment); // => [3]

Referential Transparency

如果一个表达式可以被替换为实际的值而不影响程序的运行结果,那么我们就说这个表达式是 referentially transparent:

const greet = () => “Hello World!”;

以上面代码为例,任何调用 greet() 的地方都可以替换为 “Hello World!” 而不影响程序的执行结果。

Equational Reasoning

如果一个应用由多个表达式组合而成,且每个表达式都没有 side effect,那么这个应用就可以由部分推导出整体。

Lambda

匿名函数,本质上是一个 value:

function(a){   return a + 1;};(a) => a + 1;// Lambda 常用语高阶函数中[1, 2].map((a) => a + 1); // = [2, 3]// Lambda 作为 value 被赋值给变量let addOne = (a) => a + 1;

Lambda Calculus

数学的分支之一,使用函数创建通用的计算模型(universal model of computation)。

Lazy evaluation

惰性求值,是一种按需执行的求值策略,只有需要某个值时才会执行相关的表达式。在函数式编程语言中,这一特性可用于构造无限列表。

const rand = function*() {   while (true) {       yield Math.random();    }}const randIter = rand();randIter.next().value; // 每次执行 next() 函数都会返回一个新的随机数// 有且只有在执行 next() 的时候才会返回新值

Monoid

Monoid,通过一个函数“合并”两个同类型数据后返回相同的数据类型。最简单的 monoid 就是两数相加:

1 + 1; // => 2

这里的 + 就是上面所说的“合并”函数。Monoid 中存在恒等式的概念:

1 + 0// => 1// 这里的 0 就是恒等式// Monoid 还必须满足结合律1 + (2 + 3) === (1 + 2) + 3; // => true// 数组的 concat() 操作可以构造一个 monoid[1, 2].concat([3, 4]); // => [1, 2, 3, 4]// 空数组可以视为是恒等式[1, 2].concat([]); // => [1, 2]

如果知道了一个函数的的恒等式和“合并”函数 compose,函数本身就是一个 monoid:

const identity = (a) => a;const compose = (f, g) => (x) => f(g(x));compose(foo, identity) ≍ compose(identity, foo) ≍ foo

Monad

Monad,是一个拥有 of 和 chain 函数的数据类型,chain 类似于 map,但它会输出非嵌套形式的结果:

[‘cat,dog’, ‘fish,bird’].chain((a) => a.split(‘,’)) // => [‘cat’, ‘dog’, ‘fish’, ‘bird’][‘cat,dog’, ‘fish,bird’].map((a) => a.split(‘,’)) // => [[‘cat’, ‘dog’], [‘fish’, ‘bird’]]

在其他函数式编程语言中,of 也被称为 return,chain 也被称为 flatmap 和 bind。

Comonad

Comonad,拥有 extract 和 extend 函数的数据类型:

const CoIdentity = (v) => ({   val: v,    extract() { return this.val },    extend(f) { return CoIdentity(f(this)) }})// extract() 可以从 functor 中取值CoIdentity(1).extract() // => 1// extend() 可以返回新的 comonadCoIdentity(1).extend(co => co.extract() + 1) // => CoIdentity(2)

Applicative Functor

Applicative Functor,是拥有 ap 函数的数据类型,ap 函数可以将 functor 中的值转化为其他 functor 中的同类型值:

[(a) => a + 1].ap([1]) // => [2]

这一特性对于多个 applicative functor 需要接收多个参数时,就显得很有用:

const arg1 = [1, 2];const arg2 = [3, 4];const add = (x) => (y) => x + y;const partiallyAppliedAdds = [add].ap(arg1); // => [(y) => 1 + y, (y) => 2 + y]partiallyAppliedAdds.ap(arg2); // => [4, 5, 5, 6]

Morphism

态射,一个转换函数。

Isomorphism

同构转换,相同数据下不同结构之间的转换。举例来说,2D 坐标既可以存储为数组 [2, 3] 也可以存储为 { x: 2, y: 3 }:

const pairToCoords = (pair) => ({x: pair[0], y: pair[1]})const coordsToPair = (coords) => [coords.x, coords.y]coordsToPair(pairToCoords([1, 2])) // => [1, 2]pairToCoords(coordsToPair({x: 1, y: 2})) // => { x: 1, y: 2 }

Setoid

Setoid,拥有 equals 函数的数据类型,可用于与其他同类型的数据进行比较。为 Array 类型添加 equals 函数使其成为 Setoid:

Array.prototype.equals = (arr) => {   const len = this.length   if (len !== arr.length) {       return false    }   for (let i = 0; i < len; i++) {       if (this[i] !== arr[i]) {           return false        }    }   return true}[1, 2].equals([1, 2]) // => true[1, 2].equals([0]) // => false

Semigroup

Semigroup,拥有 concat 函数的数据类型,可以与同类型数据进行合并:

[1].concat([2]) // => [1, 2]

Foldable

Foldable,拥有 reduce 函数的数据类型,可以将 Foldable 的实例转换为其他数据类型:

const sum = (list) => list.reduce((acc, val) => acc + val, 0);sum([1, 2, 3]) // => 6

Traversable

TODO

Type Signatures

类型签名,在 JavaScript 中通常会在注释中写明当前函数的参数类型和返回值类型,虽然各种语言的类型签名不同,但通常与以下示例相似:

// functionName :: firstArgType -> secondArgType -> returnType// add :: Number -> Number -> Numberconst add = (x) => (y) => x + y// increment :: Number -> Numberconst increment = (x) => x + 1

如果某个函数要作为参数传递给其他函数,那么在类型签名中需要使用括号包裹起这个函数的类型信息:

// call :: (a -> b) -> a -> bconst call = (f) => (x) => f(x)

上面示例中的 a、b 表示参数可以是任何数据类型的,但在下面的代码中,map 的类型签名表示: f 是一个函数,f 接收一个 a 类型的参数,返回一个 b 类型的值,同时 map 是一个柯里化的函数,其第二个接收一个列表形式的 a 类型参数,并返回列表形式的 b 类型参数:

// map :: (a -> b) -> [a] -> [b]const map = (f) => (list) => list.map(f)

Union type

联合类型,表示将多个类型信息放入一个类型变量中。JavaScript 中没有类型机制,所以让我们假设有一个类型变量 NumOrString,它表示 Number 或者 String 类型。+ 运算符在 JavaScript 中既可用于 Number,也可用于 String,所以我们使用 NumOrString 定义 + 的输入输出类型信息:

// add :: (NumOrString, NumOrString) -> NumOrStringconst add = (a, b) => a + b;add(1, 2); // => Number 3add(‘Foo’, 2); // => String “Foo2″add(‘Foo’, ‘Bar’); // => String “FooBar”

Product type

product type 同样包含多种基本类型:

// point :: (Number, Number) -> {x: Number, y: Number}const point = (x, y) => ({x: x, y: y});

Option

Option,是 union type 的特例,它只包含两种类型 Some 和 None。Option 常用于表示那些不确定是否返回值的函数:

// Naive definitionconst Some = (v) => ({   val: v,    map(f) {       return Some(f(this.val));    },    chain(f) {       return f(this.val);    }});const None = () => ({    map(f){       return this;    },    chain(f){       return this;    }});// maybeProp :: (String, {a}) -> Option aconst maybeProp = (key, obj) => typeof obj[key] === ‘undefined’ ? None() : Some(obj[key]);

使用 chain 函数执行链式调用可以返回具体的 Option:

// getItem :: Cart -> Option CartItemconst getItem = (cart) => maybeProp(‘item’, cart);// getPrice :: Item -> Option Numberconst getPrice = (item) => maybeProp(‘price’, item);// getNestedPrice :: cart -> Option aconst getNestedPrice = (cart) => getItem(obj).chain(getPrice);getNestedPrice({}); // => None()getNestedPrice({item: {foo: 1}}); // => None()getNestedPrice({item: {price: 9.99}}); // => Some(9.99)

某些语言中使用 Maybe 表示 Option,使用 Just 表示 Some,使用 Nothing 表示 Node。

查看文章…

函数式编程术语解析

翻译 · 原文地址

函数式编程蔚然成风,越来越多的开源项目、技术交流在使用函数式编程的术语降低开发或沟通成本,这无形中对不了解函数式编程的开发者造成了一定的学习门槛,翻译本文的初衷就是要普及函数式编程的基本知识,从新的角度扩展编程思维。至于为什么要使用 JavaScript 演示函数式编程,一方面是因为 JavaScript 的特性在很多方面与函数式编程浑然天成,另一方面是因为 JavaScript 是世界上最 XX 的语言……

Arity

指函数的参数数量,由 -ary-ity 这两个英文后缀拼接而成:

const sum = (a, b) => a + b;

const arity = sum.length;
console.log(arity); 
// => 2

Higher-Order Functions

高阶函数,此类函数可以接收其他函数作为参数,也可以返回一个函数作为返回值:

const filter = (pred, xs) => {
    const result = [];
    for (let idx = 0; idx < xs.length; idx++) {
        if (pred(xs[idx])) {
            result.push(xs[idx]);
        }
    }
    return result;
};

const is = (type) => (x) => Object(x) instanceof type;

filter(is(Number), [0, '1', 2, null]); 
// => [0, 2]

Partial Application

偏函数,在原函数的基础上预填充(pre-filling)部分参数并返回的新函数:

// 下面是一个创建偏函数的辅助函数
const partial = (f, ...args) => (...moreArgs) => f(...args, ...moreArgs);

const add3 = (a, b, c) => a + b + c;

// 预填充 (add3, 2, 3) 三个参数,空置最后一个参数,返回一个新的函数
const fivePlus = partial(add3, 2, 3); // (c) => 2 + 3 + c

fivePlus(4); 
// => 9

JavaScript 中的 Function.prototype.bind() 函数是创建偏函数的最简单方式:

const add1More = add3.bind(null, 2, 3); 
// => (c) => 2 + 3 + c

Currying

柯里化,将一个接收多个参数的函数转化为单参数函数的方式,转化后的函数每次只接收一个参数,然后返回一个新函数,新函数可以继续接收参数,直到接收到所有的参数:

const sum = (a, b) => a + b;

sum(2, 3)
// => 6

const curriedSum = (a) => (b) => a + b;

curriedSum(40)(2) 
// => 42.

const add2 = curriedSum(2); 
// (b) => 2 + b

add2(10) 
// => 12

Function Composition

函数合成,接收多个函数作为参数并返回一个新函数的方式,新函数按照传入的参数顺序,从右往左依次执行,前一个函数的返回值是后一个函数的输入值:

const compose = (f, g) => (a) => f(g(a))

const floorAndToString = compose((val) => val.toString(), Math.floor)

floorAndToString(121.212121) 
// => "121"

Purity

一个纯函数需要满足两个条件,第一是函数的返回值只能由输入值(函数接收的参数)决定,也就是说纯函数接收相同的参数会返回相同的值;第二是纯函数不会对自身作用域之外的运行环境产生副作用(side effects),比如说不会改变外部环境中变量的值,这会被认为是不安全的行为:

let greeting;
const greet = () => greeting = "Hi, " + window.name;

// greet() 执行时更改了外部环境的变量
greet(); 
// => "Hi, Brianne"

纯函数示例:

const greet = (name) => "Hi, " + name ;

greet("Brianne") 
// => "Hi, Brianne"

Side effects

如果函数或表达式与其自身作用域之外的可变数据(mutable data)发生了读写操作,那么此时函数和表达式就产生了副作用:

let greeting;
const greet = () => greeting = "Hi, " + window.name;

// greet() 执行时更改了外部环境的变量
greet(); 
// => "Hi, Brianne"

// new Date() 是可变数据
const differentEveryTime = new Date();

// 这里表示系统接收到的输入值是不确定的,是一种可变数据
console.log("IO is a side effect!");

Idempotent

幂等,同一个函数使用相同的参数嵌套执行多次的结果与执行一次的结果相同:

f(f(f(x)))=f(x)f(…f(f(x))…)=f(x)

Math.abs(Math.abs(10))

sort(sort(sort([2,1])))

Point-Free Style

point-free style 是一种不显式向函数传递参数的代码风格,通常需要柯里化和高阶函数来实现:

const map = (fn) => (list) => list.map(fn);
const add = (a) => (b) => a + b;

// Not points-free
// numbers 是一个显式传递的参数
const incrementAll = (numbers) => map(add(1))(numbers);

// Points-free
// add(1) 的返回值隐式传递给了 map,作为 map 的 list 参数
const incrementAll2 = map(add(1));

point-free style 的函数看起来就像是一个赋值表达式,没有使用我们常见的 function=> 等来声明其接收的参数。

Predicate

断言,一个返回布尔值的函数:

const predicate = (a) => a > 2;

[1, 2, 3, 4].filter(predicate); 
// => [3, 4]

Contracts

TODO

Guarded Functions

TODO

Categories

categories 内部都绑定了具体的函数用于约束或执行特定的逻辑,比如 Monoid。

Value

任何可以赋值给变量的值都可以称为 value:

5
Object.freeze({name: 'John', age: 30}) // The `freeze` function enforces immutability.
(a) => a
[1]
undefined

Constant

常量,初始化后不能再次执行赋值操作的数据类型:

const five = 5
const john = { name: 'John', age: 30 }

// 因为常量不可变,所以下面表达式一定为 true
john.age + five === ({ name: 'John', age: 30 }).age + (5)

常量具有 referentially transparent 的特性,也就是说将程序中出现的常量替换为它们实际的值,并不会影响程序的结果。译者话外:实际上在 JavaScript 中的 const 所声明的常量并不是完全稳定的,使用 Immutable.js 演示更加恰当:

const five = fromJS(5);
const john = fromJS({name: 'John', age: 30})

john.get('age') + five === ({ name: 'John', age: 30 }).age + (5)

f(g()) === g

Functor

functor 都拥有 map 函数,并且在执行 map 之后会返回一个新的 functor:

object.map(x => x) === object

object.map(x => f(g(x))) === object.map(g).map(f)

JavaScript 中最常见的 functor 就是数组类型的实例:

[1, 2, 3].map(x => x); 
// => [1, 2, 3]

const f = x => x + 1;
const g = x => x * 2;

[1, 2, 3].map(x => f(g(x))); 
// => [3, 5, 7]
[1, 2, 3].map(g).map(f);     
// => [3, 5, 7]

Pointed Functor

pointed functor 都拥有 of 函数,用于接收和构建 functor。ES2015 提供了 Array.of 函数,所以数组实例就可以看成是 pointed functor:

Array.of(1) 
// => [1]

Lift

lift 发生在你将值放入 functor 的时候,如果你将函数 lift 进了 Applicative Functor,那么就可以使用这个函数处理传递给这个 functor 的值。某些 lift 的实现拥有 lift 或 liftA2 函数,便于在 functor 上执行相关的函数:

const mult = (a, b) => a * b;

const liftedMult = lift(mult); 
// => this function now works on functors like array

liftedMult([1, 2], [3]); 
// => [3, 6]
lift((a, b) => a + b)([1, 2], [3, 4]); 
// => [4, 5, 5, 6]

lift 一个单参数的函数非常类似于 map 操作:

const increment = (x) => x + 1;

lift(increment)([2]); 
// => [3]
[2].map(increment); 
// => [3]

Referential Transparency

如果一个表达式可以被替换为实际的值而不影响程序的运行结果,那么我们就说这个表达式是 referentially transparent:

const greet = () => "Hello World!";

以上面代码为例,任何调用 greet() 的地方都可以替换为 "Hello World!" 而不影响程序的执行结果。

Equational Reasoning

如果一个应用由多个表达式组合而成,且每个表达式都没有 side effect,那么这个应用就可以由部分推导出整体。

Lambda

匿名函数,本质上是一个 value:

function(a){
    return a + 1;
};

(a) => a + 1;

// Lambda 常用语高阶函数中
[1, 2].map((a) => a + 1); 
// = [2, 3]

// Lambda 作为 value 被赋值给变量
let addOne = (a) => a + 1;

Lambda Calculus

数学的分支之一,使用函数创建通用的计算模型(universal model of computation)。

Lazy evaluation

惰性求值,是一种按需执行的求值策略,只有需要某个值时才会执行相关的表达式。在函数式编程语言中,这一特性可用于构造无限列表。

const rand = function*() {
    while (true) {
        yield Math.random();
    }
}

const randIter = rand();
randIter.next().value; 
// 每次执行 next() 函数都会返回一个新的随机数
// 有且只有在执行 next() 的时候才会返回新值

Monoid

Monoid,通过一个函数“合并”两个同类型数据后返回相同的数据类型。最简单的 monoid 就是两数相加:

1 + 1; 
// => 2

这里的 + 就是上面所说的“合并”函数。Monoid 中存在恒等式的概念:

1 + 0
// => 1
// 这里的 0 就是恒等式

// Monoid 还必须满足结合律
1 + (2 + 3) === (1 + 2) + 3; 
// => true

// 数组的 concat() 操作可以构造一个 monoid
[1, 2].concat([3, 4]); 
// => [1, 2, 3, 4]

// 空数组可以视为是恒等式
[1, 2].concat([]); 
// => [1, 2]

如果知道了一个函数的的恒等式和“合并”函数 compose,函数本身就是一个 monoid:

const identity = (a) => a;
const compose = (f, g) => (x) => f(g(x));

compose(foo, identity) ≍ compose(identity, foo) ≍ foo

Monad

Monad,是一个拥有 ofchain 函数的数据类型,chain 类似于 map,但它会输出非嵌套形式的结果:

['cat,dog', 'fish,bird'].chain((a) => a.split(',')) 
// => ['cat', 'dog', 'fish', 'bird']

['cat,dog', 'fish,bird'].map((a) => a.split(',')) 
// => [['cat', 'dog'], ['fish', 'bird']]

在其他函数式编程语言中,of 也被称为 returnchain 也被称为 flatmapbind

Comonad

Comonad,拥有 extractextend 函数的数据类型:

const CoIdentity = (v) => ({
    val: v,
    extract() { return this.val },
    extend(f) { return CoIdentity(f(this)) }
})

// extract() 可以从 functor 中取值
CoIdentity(1).extract() 
// => 1

// extend() 可以返回新的 comonad
CoIdentity(1).extend(co => co.extract() + 1) 
// => CoIdentity(2)

Applicative Functor

Applicative Functor,是拥有 ap 函数的数据类型,ap 函数可以将 functor 中的值转化为其他 functor 中的同类型值:

[(a) => a + 1].ap([1]) 
// => [2]

这一特性对于多个 applicative functor 需要接收多个参数时,就显得很有用:

const arg1 = [1, 2];
const arg2 = [3, 4];
const add = (x) => (y) => x + y;

const partiallyAppliedAdds = [add].ap(arg1); 
// => [(y) => 1 + y, (y) => 2 + y]

partiallyAppliedAdds.ap(arg2); 
// => [4, 5, 5, 6]

Morphism

态射,一个转换函数。

Isomorphism

同构转换,相同数据下不同结构之间的转换。举例来说,2D 坐标既可以存储为数组 [2, 3] 也可以存储为 { x: 2, y: 3 }

const pairToCoords = (pair) => ({x: pair[0], y: pair[1]})
const coordsToPair = (coords) => [coords.x, coords.y]

coordsToPair(pairToCoords([1, 2])) 
// => [1, 2]

pairToCoords(coordsToPair({x: 1, y: 2})) 
// => { x: 1, y: 2 }

Setoid

Setoid,拥有 equals 函数的数据类型,可用于与其他同类型的数据进行比较。为 Array 类型添加 equals 函数使其成为 Setoid:

Array.prototype.equals = (arr) => {
    const len = this.length
    if (len !== arr.length) {
        return false
    }
    for (let i = 0; i < len; i++) {
        if (this[i] !== arr[i]) {
            return false
        }
    }
    return true
}

[1, 2].equals([1, 2]) 
// => true
[1, 2].equals([0]) 
// => false

Semigroup

Semigroup,拥有 concat 函数的数据类型,可以与同类型数据进行合并:

[1].concat([2]) 
// => [1, 2]

Foldable

Foldable,拥有 reduce 函数的数据类型,可以将 Foldable 的实例转换为其他数据类型:

const sum = (list) => list.reduce((acc, val) => acc + val, 0);
sum([1, 2, 3]) 
// => 6

Traversable

TODO

Type Signatures

类型签名,在 JavaScript 中通常会在注释中写明当前函数的参数类型和返回值类型,虽然各种语言的类型签名不同,但通常与以下示例相似:

// functionName :: firstArgType -> secondArgType -> returnType

// add :: Number -> Number -> Number
const add = (x) => (y) => x + y

// increment :: Number -> Number
const increment = (x) => x + 1

如果某个函数要作为参数传递给其他函数,那么在类型签名中需要使用括号包裹起这个函数的类型信息:

// call :: (a -> b) -> a -> b
const call = (f) => (x) => f(x)

上面示例中的 ab 表示参数可以是任何数据类型的,但在下面的代码中,map 的类型签名表示: f 是一个函数,f 接收一个 a 类型的参数,返回一个 b 类型的值,同时 map 是一个柯里化的函数,其第二个接收一个列表形式的 a 类型参数,并返回列表形式的 b 类型参数:

// map :: (a -> b) -> [a] -> [b]
const map = (f) => (list) => list.map(f)

Union type

联合类型,表示将多个类型信息放入一个类型变量中。JavaScript 中没有类型机制,所以让我们假设有一个类型变量 NumOrString,它表示 Number 或者 String 类型。+ 运算符在 JavaScript 中既可用于 Number,也可用于 String,所以我们使用 NumOrString 定义 + 的输入输出类型信息:

// add :: (NumOrString, NumOrString) -> NumOrString
const add = (a, b) => a + b;

add(1, 2); 
// => Number 3
add('Foo', 2); 
// => String "Foo2"
add('Foo', 'Bar'); 
// => String "FooBar"

Product type

product type 同样包含多种基本类型:

// point :: (Number, Number) -> {x: Number, y: Number}
const point = (x, y) => ({x: x, y: y});

Option

Option,是 union type 的特例,它只包含两种类型 SomeNone。Option 常用于表示那些不确定是否返回值的函数:

// Naive definition
const Some = (v) => ({
    val: v,
    map(f) {
        return Some(f(this.val));
    },
    chain(f) {
        return f(this.val);
    }
});

const None = () => ({
    map(f){
        return this;
    },
    chain(f){
        return this;
    }
});

// maybeProp :: (String, {a}) -> Option a
const maybeProp = (key, obj) => typeof obj[key] === 'undefined' ? None() : Some(obj[key]);

使用 chain 函数执行链式调用可以返回具体的 Option

// getItem :: Cart -> Option CartItem
const getItem = (cart) => maybeProp('item', cart);

// getPrice :: Item -> Option Number
const getPrice = (item) => maybeProp('price', item);

// getNestedPrice :: cart -> Option a
const getNestedPrice = (cart) => getItem(obj).chain(getPrice);

getNestedPrice({}); 
// => None()
getNestedPrice({item: {foo: 1}}); 
// => None()
getNestedPrice({item: {price: 9.99}}); 
// => Some(9.99)

某些语言中使用 Maybe 表示 Option,使用 Just 表示 Some,使用 Nothing 表示 Node

from:http://pinggod.com/2016/函数式编程术语解析/

函数式程序设计的另类指南

函数式程序设计的另类指南

Authorized translation from the English language edition, entitled Functional Programming For The Rest of Us, by Slava Akhmechet.
Chinese simplified language version is translated by Yifan Peng. Part of the translation is from lihaitao(lihaitao@gmail.com)
Copyright (c) 2014 Yifan Peng.
All rights reserved.

本文中文简体字版由Slava Akhmechet授权。未经书面许可,请勿复制或抄袭。


目录


简介

程序员拖沓成性,每天到办公室以后,泡泡咖啡、查查邮箱、读读RSS上的回复,看看新闻,到技术网站点击一下最新的文章,然后在编程论坛的相关版面浏览公共讨论区,并不厌其烦地刷新页面,以免漏掉任何一条留言。午饭后盯着IDE没几分钟,再次检查邮箱,或者冲一杯新的咖啡。就这样不知不觉中,结束了一天。

如果你浏览的网站很对路的话,每隔几天都会发现一些很有挑战性的文章——你很难快速通读它们,于是将之束之高阁,直到有一天突然发现自己已经有了一个长长的链接列表和一个堆满了PDF文件的目录。这时你会幻想到一个人迹罕至的森林木屋,苦读一年以学会这些技术。当然,每天清晨当你沿着林中小溪散步的时候,如果有人帮你带饭、清理垃圾就更好了。

我不知道你的列表是什么,但我的列表却包含了一大堆关于函数式程序设计的文章。这些文章都很难读懂。它们用枯燥的学院派语言写成,即使“在华尔街行业浸淫十年的专家”也不能理解函数式程序设计都在探讨些什么。如果你去问花旗集团或德意志银行的项目经理1,为什么选择了JMS 而不是Erlang,他们可能会说:不能在产业级的应用中使用学院派语言。但问题是,一些最复杂、有着最严格需求的系统却是用函数式程序设计元素写成的。所以,这些说法不能让人信服。

的确,有些关于函数式程序设计的文章和论文很难理解,但它们原本并不是这么晦涩。产生隔阂的原因完全是历史性的。函数式程序设计的概念并不难理解。本文就是“简易的函数式程序设计导论”,是一座沟通命令式(imperative)思维模式和函数式程序设计的桥梁。去冲杯咖啡回来继续读下去吧。可能你的同事很快就会开始取笑你对函数式编程发表的观点了。

那么什么是函数式程序设计呢?它是怎么产生的?它可以被驾驭吗?如果它真如其倡导者所言那么有用,为什么没有在行业中得到广泛使用呢?为什么好像只有那些博士才使用它?最重要的是,为什么它就TMD这么难学?闭包(closure)、continuation、currying、惰性赋值(lazy evaluation)、no side effects business究竟是些什么东西?一个项目如果没有大学参与,能不能使用函数式语言?为什么它看上去那么不友好、不亲切?我们马上会解答这些疑问。首先让我来解释实际应用和学术文章之间,有着产生巨大隔阂的原因。其实答案简单得就像在公园散一次步。

信步游园

启动时间机器,我们来到两千多年前的一个公园里。具体时间大约是公元前380年的一个春光明媚的周日。在雅典城外的橄榄树树荫里,柏拉图(Plato)和一个英俊的奴隶小男孩正朝着学院走去。那天天气很好,晚饭也不错。他们边走边讨论一个哲学问题。

为了使问题更有教育意义,柏拉图小心地挑选着词句:“瞧这两个学生,你认为谁更高呢?”小男孩看了看那两个站在水池中的人,说,“他们俩差不多高”。柏拉图问:“你说‘差不多’是什么意思?”。“恩……我在这儿看他们是一样高的,不过我肯定如果走近些就会看出他们的差别。”

柏拉图笑着把这个孩子引向正确的思路。“那么你是说,这个世界上没有完全相同的两个东西了?”小男孩想了一会儿回答,“对。我想即使我们看不到,任何事物之间总有一些区别。”正中下怀!“那么如果这世上没有完全相同的两个东西,你怎么理解‘完全’相等这个概念呢?”小男孩有点被问住了,说:“我不知道”。

这就是我们第一次尝试理解数学的本源。柏拉图提出,世界上所有的事情都只是趋近于完美。他同时也意识到,尽管我们无法真正碰触到完美的事情,但是我们可以理解它。因此完美的数学仅存在于另一个世界中,而我们可以通过和那个世界的某种联系在一定程度上认知它。比如,虽然我们无法看到绝对完美的圆,但是我们知道什么是圆,并且能够用公式表达它。那什么是数学呢?为什么宇宙可以用数学定理来描述?数学可以描述宇宙中的所有现象吗?2

数学哲学是一个很复杂的课题。像大多数哲学学科一样,它更倾向于提出问题而不是给出答案。很多得出的共识都围绕着一个事实:数学真的是个谜。我们首先给出一些基本的、互不冲突的原理,以及一些可以操作这些原理的规则;然后我们组合这些规则生成更复杂的规则。数学家把这种方法叫做“形式系统”或“微积分”。如果愿意,我们可以很快为俄罗斯方块写出一个形式系统。实际上,一款俄罗斯方块游戏本身就是一个形式系统,只不过游戏采用了非数学的表现形式。

半人马阿尔法行星上的毛毛生物文明不能理解我们对于俄罗斯方块或者圆的范式,因为它们唯一可以感知世界的器官可能只有嗅觉。他们也许永远不会发现俄罗斯方块的范式,但很可能会有一个圆的范式。而我们也可能无法知道如何通过嗅觉描述一个圆,因为我们的嗅觉没有那么灵敏。可是一旦我们能理解那一范式的表示形式(比如通过各种传感器和标准解码技术来理解这种语言),其底层的概念就可被任何智能文明所理解。

有趣的是,即便宇宙中从没有过智能文明,俄罗斯方块和圆的范式仍然存在,只是没有人发现它们而已。如果一种智能文明出现了,他应该能发现一些形式系统来描述宇宙的规律。但他还是不大可能搞一个俄罗斯方块, 因为宇宙中再没有和它相似的东西。在现实世界中这类无用的形式系统或迷题的例子数不胜数,俄罗斯方块只是其中一个典型的例子。我们甚至不能确定自然数是否是对客观世界的完整近似。比如我们可以很容易的想出一个特别大的数字,它无法表达我们世界中的任何事物,因为我们的世界有限的,而这个数近乎无穷。

历史一瞥3

再次启动时间机器,这次我们回到二十世纪30年代。大萧条正在蹂躏着那个新旧交替的时代。空前的经济下滑影响着几乎所有阶层的家庭。只有少数人还能够保持着饥谨危机前的安逸,比如普林斯顿大学的数学家们。

歌特式的新办公室给普林斯罩上天堂般的幸福光环。来自世界各地的逻辑学家被邀请到此成立一个新学部。那时的美国人民已很难弄到一块面包,但是在普林斯顿,你可以在高高的穹顶下、精致雕凿的木质墙饰边,整日的品茶讨论,或在楼外的林荫中款款慢步。

阿隆左·丘奇(Alonzo Church)就是其中一位享受这种近于奢华的数学家。他在普林斯顿获得本科学位后被邀继续留在研究生院攻读。阿隆左认为普林斯顿的建筑过于浮华,所以他很少一边喝茶一边与人讨论数学,他也不喜欢到林中散步。他有些孤僻,因为似乎只有独自一人时,他才能以最高的效率工作。尽管如此,他仍与另一些同样居住在普林斯顿的人保持着联系,比如阿兰·图灵(Alan Turing),约翰·冯·诺依曼(John von Neumann),和库尔特·冈特(Kurt Gödel)。

这四个人都对形式系统很感兴趣。他们致力于解决抽象的数学难题,而不太留意现实的世界。这些难题的共同之处就是计算:如果计算机能有无限的计算能力,哪些问题可以被解决?哪些问题可以被自动解决?哪些问题依旧无法解决?为什么不能被解决?基于不同设计的各种计算机是否具有相同的计算能力?

通过和其它人的合作,阿隆左·丘奇提出了一个被称为lambda演算的形式系统。这个系统本质上是一种程序设计语言。它可以运行在具有无限计算能力的机器上。lambda演算由一些函数构成,这些函数的输入输出也是函数。函数用希腊字母lambda标识,因此整个形式系统也叫lambda4。通过这一形式系统,阿隆左就可以对上述诸多问题进行推理并给出结论性的答案。

在同一时间,阿兰·图灵也在进行着相似的工作。他提出了一个完全不同的形式系统(现在被称为图灵机),并使用这一系统得出了和阿隆左相似的结论。事后证明,图灵机和lambda的演算能力是等价的。

我们的故事本可到此结束。如果第二次世界大战没有在那时打响,我现在就可以歇笔,而你也可以浏览下一个页面了。那时整个世界笼罩在战争的火光和硝烟之中,美国陆军和海军大量使用炮弹。为了改进炮弹的精确度,部队雇佣了大批数学家通过计算微分方程来给出弹道发射的轨迹。很显然这项工作人力浩繁,因此人们开始着手开发各种设备来攻克这个难关。第一个解出了弹道轨迹的机器是IBM的Mark I——它重达5吨,有75万个部件,每秒可以完成三次操作。

竞争当然没有就此结束。1949年,EDVAC(Electronic Discrete Variable Automatic Computer)被推出并获得了巨大的成功。这是冯·诺依曼架构的第一个具体实现,实际上也是图灵机的第一个实现。而与此同时,阿隆左·丘奇则没有那么幸运。

二十世纪五十年代,一位MIT的教授John McCarthy(也是普林斯顿毕业生)对阿隆左·丘奇的工作产生了兴趣。1958年,他发布了Lisp语言(List Processing language)。Lisp的不同之处在于,它在冯·诺依曼计算机上实现了阿隆左·丘奇的lambda演算!很多计算机科学家开始意识到Lisp的表达能力。1973年,MIT人工智能实验室的一帮程序员开发了被称为Lisp机器的硬件,于是阿隆左的lambda演算系统终于在硬件上实现了!

函数式程序设计

函数式程序设计是对阿隆左·丘奇思想的一种实现。但并非所有的lambda演算都被实现了,因为lambda演算原本不是为有物理限制的计算机设计的。因此,函数式程序设计和面向对象程序设计一样,只是一系列理念,而不是严格的使用手册。如今有很多种函数式编程语言,它们各自采用了不同的方法。在本文中,我将使用Java来编写函数式程序,并且解释函数式语言的常用特性(的确,如果你有受虐倾向,你可以用Java写出函数式程序)。在下面几章中,我将会对Java稍作修改,以使其成为一个可用的函数式编程语言。那我们开始吧。

lambda演算被设计用来解决计算问题,所以函数式程序设计主要用于处理计算。正如它的名字那样,程序用函数来完成所有操作。函数是函数式程序设计的基本单位。它几乎无处不在。即使最简单的计算也由函数构成。甚至变量(variable)都由函数取代。在函数式编程中,变量只是表达式的别名(这样我们就不必把所有东西打在一行里)。变量是不能被更改的并且只能被赋值一次。在Java中,这意味着所有变量都要被声明为final(或C++中的const)。在函数式编程中没有非final的变量。

1
2
final int i = 5;
final int j = i + 3;

因为函数式编程中的所有变量都是final的,所以可以提出这样两个有趣的假设:(1)没有必要总是写出关键字final,(2)没有必要再把变量称为变量。那么现在我们对Java作出两个修改:(1)在我们的函数式Java中,所有变量默认都是final的,(2)我们将变量称为符号(symbol)。

现在你可能会奇怪,用我们新创造的语言还能写出复杂的程序吗?如果每个符号都是不可变(non-mutalbe)的,我们就无法改变任何事情的状态!其实事实并非如此。在阿隆左研究lambda演算时,他并不想维护某个状态,并且在未来修改它。他关注的是如何对数据进行操作(这也通常被称为“演算体(caculating stuff)”。不管怎么说,既然lambda演算已被证明与图灵机等价,命令式程序能做的事情它应该也能做。但是我们应该怎么做呢?其实函数式程序也能保存状态,只是它使用的是函数,而不是变量。函数式程序将状态保存在函数的参数中,而这些参数又保存在“栈”上。如果你想保存某个状态并且想每隔一段时间就去修改它,你可以写个递归函数。比如,我们可以写个能够翻转Java字符串的函数。记住,我们声明的每个变量默认都是final的5

1
2
3
4
5
6
7
String reverse(String arg) {
  if(arg.length == 0) {
    return arg;
  } else {
    return reverse(arg.substring(1, arg.length)) + arg.substring(0,1);
  }
}

这个函数很慢而且特别消耗内存6。它慢是因为它不停的调用自己,而它耗内存是因为它不断的分配对象。但是它确实是一个典型的函数式函数。你可能会问,怎么会有人这样写程序呢?下面就让我慢慢道来。

函数式编程的优点

你可能会认为我根本无法对上面那个变态的函数给出合理的解释。我开始学习函数式编程时,也这么想。不过事实证明我错了。有许多很好的理由来支持这样的写法,当然其中一些是主观因素。比如,有人号称函数式程序易于理解。我不会拿这些理由出来说事,因为小孩子都知道:情人眼里出西施。不过我还能找到很多客观理由。

单元测试

因为函数式编程的每一个符号都是final的,所以没有函数能产生副作用。因为你不能在某个地方修改值,也不能在一个函数中修改其作用域外别的函数使用的值(比如类成员或全局变量),所以计算(或者运行)一个函数,只能得到它的返回值,而唯一可以改变该返回值的是这个函数的参数。

对单元测试者来说,这是梦寐以求的。你可以测试程序中的每个函数,并且只关心它的输入参数。你不用理会函数的调用关系,也不用精心设置外部状态。唯一需要做的就是把需要测试的极端情况输入给函数。比起使用命令式编程,如果函数式程序中的每个函数都通过了单元测试,那么你会对整个程序的质量有更大的信心。在Java或C++中,只检查函数的返回值是不够的——我们还必须验证这个函数可能修改的外部状态。但是在函数式程序中,这种情况永远不会发生。

调试

如果一段函数式程序没有按照你所希望的那样执行,调试起来也是轻而易举。因为函数式程序中的bug与之前执行的代码无关,所以你总是可以复现遇到的问题。在命令式程序中,有些bug会时隐时现,这是由于该函数可能会依赖其他函数提供的外部状态,而那些其他的函数可能才是问题的关键。因此你必须一并检查它们。而这种调试看似和这个Bug并无直接关系。函数式程序则不同——如果一个函数的返回值错了,它永远是错的,这与你之前运行了什么代码无关。

一旦你复现了问题,寻其根源将毫不费力甚至颇有乐趣。给你的程序打个断点,然后看看栈中的情况。和命令式编程一样,你可以检查栈里每一次函数调用的参数。在命令式编程中,这是不够的,因为函数依赖于成员变量、全局变量、以及其他类的状态(这些类还可能依赖其他的成员变量、全局变量、甚至更多其他的类)。函数式程序里函数依赖于它的参数,而那些信息就在你的面前!另外,在命令式程序里,只检查一个函数的返回值不能够让你确信这个函数已经可以正常工作了。你还需要逐个检查一堆作用域外的对象来看看它们是否也处于正常的状态。而对函数式程序,你要做的所有事就是查看其返回值!

沿着堆栈检查函数的参数和返回值,只要有返回值出现问题,就进入那个函数然后一步步跟踪下去。重复这个过程,你就能发现bug的位置。

并发

函数式程序就是为并发而生的。因为用不到锁(lock),所以永远不必担心死锁和竞争条件(race condition)。在函数式程序中,没有任何数据会被同一个线程修改两次,更不用说被两个不同的线程修改了。这意味着你可以不假思索地添加线程而不用担心困扰并发程序设计的常见问题。

如果这样,那么为什么没有人在大型并发应用中运用函数式编程呢?其实是有的。爱立信公司设计了一种函数式语言(Erlang)[http://www.erlang.org/],用于需要极高抗错性和可扩展性的电话交换机上。其他公司也意识到了Erlang的优势,并开始使用它。我们所说的程控交换和电信通信控制系统,需要比传统的华尔街系统更易扩展和可靠。实际上,Erlang系统并不具有很高的扩展性和可靠性——Java系统才是——但是Erlang系统坚如磐石。

并发的故事并未就此结束。即使你的程序本身就是单线程的,函数式程序的编译器仍然可以对其进行优化,使其运行于多个CPU上。来看下面这段代码。

1
2
3
String s1 = somewhatLongOperation1();
String s2 = somewhatLongOperation2();
String s3 = concatenate(s1, s2);

在函数式编程中,编译器可以对代码进行分析,发现生成字符串s1s2的函数可能非常耗时,于是对其并行运行。这种情况在命令式语言中是不可能出现的,因为每一个函数都有可能修改外部状态,而后续函数有可能依赖这些外部状态。在函数式语言里,自动分析哪些函数可以并发执行就像自动内联一样简单。从这个意义上说,具有函数式风格的程序是“不会过时的技术(future proof)”(尽管我很讨厌流行用语,但这回也要破例使用一次)。硬件厂商已经无法让CPU运行得更快了。他们只能靠增加内核数量,用并发来成倍的提高速度。当然他们没有提醒我们,只有当处理并发问题的时候,我们花的钱才会物有所值。命令式程序中只有很小一部分是可以并发的,但是在函数式程序中,所有函数天生都是可并发的。

代码热部署

过去,如果要在Windows上安装更新,就必须不断地重启计算机。即使只是安装了一款新版媒体播放器,也必须重启。Windows XP大大改进了这个问题,但仍不理想(今天工作的时候,我运行了一下Windows Update,结果除非我重启系统,那个烦人的图标会一直出现在我的系统托盘上)。Unix系统一直以来有一个更好的架构:安装更新时只需停止与其相关的组件,而不是整个操作系统。即使如此,对于大型的服务器应用程序来说,这仍旧无法让人接受。程控交换系统需要100%的时间都在运行。因为如果由于升级而无法接通紧急电话,那很可能会要人命的。同样,华尔街的公司也没有理由必须在周末暂停系统来更新软件。

理想的情况是,在完全不停止系统任何组件的情况下来更新相关的代码。在命令式程序的世界里,这是不可能的。想想在Java运行过程中卸载一个类并且加载一个新的类。即使我们真的可以这样做,这个类的所有实例也都不能用了,因为这个类的状态丢失了。我们需要复杂的版本控制代码来恢复这些状态:需要把运行中实例的都序列化,销毁它们,用新的类创建新的实例,最后载入先前被序列化的数据,并祈祷着加载代码确实能将数据迁移到新的实例中。更痛苦的是,每一次改变代码的时候,我们都必须手动编写这些迁移程序。这些迁移代码不仅要迁移实例,而且还不能破坏对象间的关系。这些听来理论上可行,但实践起来可不容易。

在函数式程序中,所有的状态都存储在栈中,并且通过参数传递给函数。这使得热部署轻而易举!实际上,我们需要做的只是比较一下工作中的代码和新版本的代码,然后部署变化的部分。剩下的工作将由一个语言工具自动完成!如果你觉得这是科幻小说,我建议你再想想。这么多年来,Erlang工程师始终在运转着的系统上直接升级

机器辅助证明和优化

函数式语言的一个有趣的特性就是它们可以用数学方式进行推理。因为函数式语言只是一个形式系统的实现,所以只要是这个形式系统能够完成的数学运算(即使只是写在纸上),用其函数式语言也可以完成。举个例子来说,编译器可以把一段代码转变成另一段运行结果相同但是更高效的代码,然后在数学上证明二者是等价的7。多年来关系数据库一直在进行着类似的优化。没有理由说这种技术无法应用在常规软件上。

另外,你可以通过这些技术来证明你的一部分程序理论上是正确的。甚至可以写一个工具来分析代码并自动生成单元测试的边界用例!这个功能对于需要设计一个坚如磐石的系统来说是无价的。如果你要设计一个心脏起搏器或者交通控制系统,这种代码分析工具是必不可少的。即便你的程序不是这样人命关天,这些工具也是你击败竞争对手的杀手锏。

高阶函数

我记得即使在了解了上面种种优点之后,自己旧会想“恩,这些的确不错,但是如果让我在一个什么都是final的语言中编程的话,我宁可不用它。”其实你误解了我的意思。将所有变量都声明为final确实显得蹩脚,但是只有在像Java这样的命令式语言中,才会如此;在函数式语言中则不会。函数式语言提供了不同的抽象工具来让你忘记你曾经习惯于修改变量。高级函数就是这样一种工具。

函数式语言中的函数不同于Java或C中的函数。除了Java能做的事,它的功能更宽泛。因此函数式语言中的函数是Java或C中函数的超集。我们模仿C语言来定义一个函数:

1
2
3
int add(int i, int j) {
  return i + j;
}

与C语言代码不同,现在我们扩展Java编译器使其支持这种记法。当我们输入上述代码后,编译器会把它转换成下面的Java代码(别忘了,所有变量都是 final 的):

1
2
3
4
5
6
7
class add_function_t {
  int add(int i, int j) {
    return i + j;
  }
}
add_function_t add = new add_function_t();

add并不是一个真正的函数。它是一个只有一个成员函数的类。现在,我们可以将add作为其他函数的参数来使用,也可以将它赋给其他的变量。在运行时,我们可以创建一个add_function_t的实例。这些实例在用过之后会被当作垃圾回收。我们把这样的对象函数称作第一级类对象,它与整数或字符串无异。我们把操作其他函数的函数(将其他函数作为参数的函数)称为高阶函数。别让这个术语吓着你,因为这和在Java中把一个类作为参数传递给另一个类,以实现类之间的操作没有任何区别。类似第一级类对象,我们把包含高阶函数的类叫做高阶类。其实没有人关心这个名字,因为Java背后没有一个强大的学术社区。

那么应该在什么时候使用高阶函数呢?又怎么用呢?我很高兴你会问到这个问题。在写一大堆代码的时候不考虑任何类层次结构。当遇到重复的代码时,把重复的部分提取成函数(幸运的是现在学校还在教这个)。当看到函数中的一段逻辑会在不同的状况下有不同的行为时,就把这个逻辑片段提取成高阶函数。有点晕?下面是我工作中遇到的一个例子。

假设有一段Java代码,它接收一段信息,将其以多种方式转换,然后转发至其他服务器上。

1
2
3
4
5
6
7
8
9
10
class MessageHandler {
  void handleMessage(Message msg) {
    // ...
    msg.setClientCode("ABCD_123");
    // ...
    sendMessage(msg);
  }
  // ...
}

现在想象一下,我们要将信息转发至两个服务器。除了第二台服务器需要另一种格式的client code外,其他一切都不变。我们应该怎么办?可以根据要转发到哪台服务器来修改client code的格式,比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MessageHandler {
  void handleMessage(Message msg) {
    // …
    if(msg.getDestination().equals("server1") {
      msg.setClientCode("ABCD_123");
    } else {
      msg.setClientCode("123_ABCD");
    }
    // …
    sendMessage(msg);
  }
  // …
}

但是这种方法不备可扩展性。如果更多的服务器加入,这个函数的长度将线性地增长。再更新代码时就会变得很麻烦。采用面向对象的方法,我们会定义一个父类MessageHandler,然后在派生类中具体实现生成client code的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
abstract class MessageHandler {
  void handleMessage(Message msg) {
    // ...
    msg.setClientCode(getClientCode());
    // ...
    sendMessage(msg);
  }
  abstract String getClientCode();
  // ...
}
class MessageHandlerOne extends MessageHandler {
  String getClientCode() {
    return "ABCD_123";
  }
}
class MessageHandlerTwo extends MessageHandler {
  String getClientCode() {
    return "123_ABCD";
  }
}

现在我们可以对每一个服务器实例化一个合适的类。添加服务器的操作变得容易维护了。但对于如此简单的修改却需要添加大量的代码。为了支持不同的client code,我们创建了两个新的类型!下面用我们修改过的、支持高阶函数的语言来实现同样的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MessageHandler {
  void handleMessage(Message msg, Function getClientCode) {
    // ...
    Message msg1 = msg.setClientCode(getClientCode());
    // ...
    sendMessage(msg1);
  }
  // ...
}
String getClientCodeOne() {
  return "ABCD_123";
}
String getClientCodeTwo() {
  return "123_ABCD";
}
MessageHandler handler = new MessageHandler();
handler.handleMessage(someMsg, getClientCodeOne);

我们没有建立新的类型与类层次构,而只是把适当的函数作为参数传入,就完成了与面向对象一样的功能。这个方案有很多优势。我们不再局限于类的层次结构中:我们可以在运行时传入新的函数,并且随时以更少的代码更大的粒度修改它们:我们的编译器能够自动高效地生成面向对象的代码(将函数转换为函数类)。并且我们获得了函数式编程的所有好处。当然函数式语言提供的抽象性远不止于此,高阶函数仅仅是个开始。

Currying

我认识的很多人都读过四人帮(GOF)的《设计模式》。任何自恋的程序员都会告诉你,这本书中讨论的模式在软件工程中具有通用性,它和使用哪门语言无关。这个说法显得颇为高深,但是有违现实。

函数式编程极具表达能力。你不需要在函数式语言中使用设计模式,因为这种高级程序设计语言可以让你只进行概念编程,从而不再需要设计模式。适配器(Adapter)模式就是这样的一个例子(适配器和外观模式(Facade)有什么区别?某人可能需要在这里高谈扩论了)。一旦语言有了叫作currying的技术,我们就不再需要适配器模式了。

适配器模式最有名的应用是Java的“默认”抽象单元——类。在函数式编程里,这个模式被应用到函数上。适配器模式将一个接口转换为另一个接口。这里有一个适配器模式的例子:

1
2
3
4
int pow(int i, int j);
int square(int i) {
  return pow(i, 2);
}

上面的代码把一个整数幂运算接口转换成为了一个平方接口。在学术圈中,这样的用法被称之为currying(得名于逻辑学家Haskell Curry,他曾将相关的数学理论形式化)。因为在函数式编程中,函数——而不是类——作为参数进行传递,因此可以用currying把函数适配到其他接口。又因为函数的接口就是其参数列表,所以currying可以减少参数的数量(如上例所示)。

因为函数式语言内建了这一技术,所以不用手动地创建一个包装了原函数的类。函数式语言会为你代劳。和之前一样,我们来扩展一下我们的语言来支持这个技术:

1
square = int pow(int i, 2);

编译器会自动为我们创建一个只有一个参数的square函数。它会在第二个参数为2的情况下调用pow函数。这段代码会编译成如下Java代码:

1
2
3
4
5
6
class square_function_t {
  int square(int i) {
    return pow(i, 2);
  }
}
square_function_t square = new square_function_t();

正如你所见,我们只是简单地包装了一下原函数。在函数式编程中,这就是currying——快速便捷的包装一个函数。你专注于你的工作,而编译器为你生成具体的代码!那么什么时候用currying?很简单:当你想用适配器模式(包装)的时候。

惰性求值

当我们采用函数式哲学以后,就可以使用惰性(或延迟)求值这一技术。在并发一节,我们已经看过如下代码:

1
2
3
String s1 = somewhatLongOperation1();
String s2 = somewhatLongOperation2();
String s3 = concatenate(s1, s2);

在一个命令式语言中,求值的顺序一目了然。因为每个函数都有可能改变外部状态,所以执行顺序必须首先是somewhatLongOperation1,然后somewhatLongOperation2,最后是concatenate。在函数式语言中,则不一定非要按照这个顺序。

我们之前讲到,如果没有函数会修改或依赖于全局变量,那么somewhatLongOperation1和somewhatLongOperation2就可以并发执行。但如果我们不想并发执行它们,就必须顺序执行它们么?不一定。只有当另一个函数需要s1和s2的时候,我们才会执行这两个函数。因此在concatenate被调用之前,我们不需要执行它们——我们甚至可以将它们的求值延迟到concatenate函数中实际用到它们的位置。如果我们将concatenate替换成另外一个函数,而这个函数带有一个条件分支。如果该分支每次只会使用两个参数中的一个,。那么我们可能永远没有必要对另一个参数求值。Haskell就是这样一门支持惰性求值的语言。Haskell不能保证每一行代码都会被按顺序执行,甚至不能保证所有代码都会被执行。这是因为只有当一段代码在需要使用的时候,Haskell才会运行那段代码。

惰性求值有利有弊。我们先来说说优点,然后再讨论如何克服缺点。

优化

惰性求值具有巨大的优化潜力。惰性编译器对待函数式代码就像数学家对待代数方程式一样:一些部分可以被约分从而不必执行,一些部分的顺序可以进行调整以提升效率,代码甚至可以被重整以降低错误。所有这些优化都能确保不会破坏代码原本的逻辑。这就是严格使用形式系统来表达程序的最大好处——代码依附于数学定律,因此可以用数学进行推理。

抽象控制结构

惰性求值提供了更高一级的抽象。这种抽象使得一些原来不可能的操作变得可行。比如下面这个控制结构:

1
2
3
unless(stock.isEuropean()) {
  sendToSEC(stock);
}

我们希望只有在stock为European的情况下才执行sendToSEC。那么应该如何实现unless?如果没有惰性求值,我们需要某种形式的宏。但是在像Haskell这样的语言中,则没必要这样做。我们可以将unless实现为一个函数:

1
2
3
4
void unless(boolean condition, List code) {
  if(!condition)
    code;
}

请注意,当分支条件是true的时候,code永远不会被执行。在执行顺序严格的语言中这样的代码是无法实现的,因为在unless调用之前,参数code已经被运行了。

无限的数据结构

惰性求值允许定义无限的数据结构。在执行顺序严格的语言中,这是很难实现的。比如一个Fibonacci数列。显然我们无法在有限的时间内计算出一个无穷列表,也不能在有限的内存里保存这个列表。Java只能定义一个Fibonacci函数来返回Fibonacci数列中某个指定位置的元素。但是Haskell可以将它抽象为一个无限的Fibonacci数列。因为该语言具有延迟的特性,所以这个数列中只有实际被用到的部分才会被求值。总之,惰性求值使我们可以抽象出许多问题,并从一个更高的层面来审视它们。(例如,我们可以在一个无穷列表上使用表处理函数)。

缺点

当然天下没有免费的午餐。惰性求值也有一些缺点。最大的问题其实就是“惰性”。许多现实问题都需要严格按顺序执行。例如下面这段代码:

1
2
System.out.println(”Please enter your name: “);
System.in.readLine();

惰性求值语言无法保证第一行会比第二行先执行!这意味着我们没法做IO操作,没法以任何有用的方式调用本地接口(因为它们相互依赖,所以必须被顺序执行),也没法与外界交互!如果引入允许顺序执行的特性,我们又将失去能够用数学进行代码推理的好处(并为此葬送与其相关的函数式编程的所有优点)。幸运的是,不是所有优点都没了。数学家们找到了许多技巧来保证,在一定函数设置下,代码可以按顺序执行。这样我们就赢得了两个世界。这些技巧包括:continuations,monads,还有uniqueness typing。在这篇文章中,我们只会讨论continuations。monads和uniqueness typing只能留到以后讨论。有趣的是,continuations除了能让代码以特定的顺序执行以外,还有很多其他优点。这点等一会儿就会提到。

Continuations

Continuations对于程序设计的意义,就像《达芬奇密码》对人类历史的意义:揭露了人类有史以来最大的假象。恩,也许没那么牛。但它在概念上的突破性至少和开方负数的意义相同。

我们学习函数时,其实基于这样一个假设:函数只能将结果返回给调用者。在这个意义上continuation是广义的函数。一个函数不一定必须要返回到其调用者,它可以返回到程序的任何地方。continuation可以是函数的一个参数,我们通过这个参数指定函数返回位置。这个描述可能听起来很复杂,我们来看看下面的代码:

1
2
int i = add(5, 10);
int j = square(i);

函数add返回15,并赋值给i,即原始调用add的地方。然后在调用square的时候使用i。注意,延迟特性的编译器无法重新排列这些代码,因为第二行代码依赖于第一行的执行。但是我们可以用延续传递方式(Continuation Passing Style,CPS)重写这段代码,用它来指定add函数直接返回到square,而不是原来的调用者:

1
int j = add(5, 10, square);

这个例子中 add 有了另一个参数 —— 一个add在结束时需要调用的函数。这里square是add的continuation。这两段代码的结果都是j=225.

这个技巧可以用于迫使惰性语言顺序执行两个表达式。接下来我们看看这个(熟悉的)IO代码:

1
2
System.out.println("Please enter your name: ");
System.in.readLine();

这两行代码彼此独立,所以编译器会根据自己希望的顺序去执行他们。但是,如果我们用CPS来重写这段代码,它们之间便有了依赖关系,编译器会按顺序执行它们:

1
System.out.println("Please enter your name: ", System.in.readLine);

这里,println需要在结束后调用readLine并且返回readLine的结果。如此这两行就能保证被有序执行,并且readLine一定会被执行(因为整个运算以最后一个值作为返回值)。Java的println会返回void,但如果假设它返回的是一个readLine能接受的抽象值,我们就解决了这个问题!当然,像这样把函数串起来,会降低代码的可读性。不过这个可以避免。我们可以在程序语言中添加一些语法规则,使我们可以像原来那样按顺序输入代码,然后编译器自动把它们串起来。这样我们就可以按希望的顺序执行代码,并且保留一切函数式编程的好处(包括按数学逻辑来解释我们的代码)。如果说到这里还有点晕的话,那么记住,一个函数就是一个只有一个成员的类实例。如果将上面的代码重写成println和readline的类实例的话,可能就清楚多了。

如果我就此结束,那将仅仅涉及到continuation的皮毛。我们可以用CPS重写整个程序,使所有函数都有一个额外的continuation参数,然后将当前函数的执行结果传进去。我们也可以将函数看成一类特殊的continuation(函数总是返回值给调用者),然后将程序转换成CPS代码。这种转换很容易被自动化(事实上,许多编译器就是这么做的)。

当我们将一个程序转换为CPS以后,每一个指令都会有continuation,一个当前函数执行后调用的函数,也就是通常的程序中的返回地址。让我从上面的代码中挑一个指令,比如add(5,10)。在CPS中,add的continuation是add调用完毕后接下来要执行的函数,但是在非CPS的程序中,continuation是什么呢?我们当然可以把它转换成CPS后来解释,但有这个必要吗?

其实没有必要。仔细看一下CPS转换过程。如果你试着去为它写一个编译器,并且好好思考如何去做的话,你会意识到CPS根本不需要栈!没有函数会像传统程序那样“返回”,它只是在执行完毕之后调用另一个函数。因此我们不用在每次调用函数时把参数都压到栈中,然后再在调用结束时把它们弹出来。我们只需要把它们存到内存块中,然后使用跳转指令。我们永远不需要原始的参数。他们不会被再次用到,因为没有函数会返回!

所以,用CPS风格写成的程序没有栈,但每个函数却有一个额外的参数来调用下一个函数。而不以CPS方式写的程序没有额外的参数,但是有栈。栈中包含了什么?一些参数和一个指向函数返回地址的内存指针。看到关系了么?栈中包含的就是continuation信息!栈中指向返回地址的指针本质上和CPS里将被调用的函数是等价的。如果你想知道add(5,10)的continuation是什么,那么你只需要检查它被执行时的栈即可。

这的确很简单。continuation和栈上指向返回地址的指针是等价的。continuation被显式传递,所以它不一定必须是函数原来被调用的地方。如果你还记得continuation就是一个函数,并且函数在我们的程序语言被编译成一个类的实例的话,你会更理解二者是一样的。这意味着给定程序中任意时间和任意位置,你都可以得到一个当前的continuation(current continuation),即当前栈的信息。

好的,这样我们就知道了什么是current continuation。有什么用呢?当我们将current continuation保存在某处时,实际上是把程序的当前状态速冻起来。这类似于操作系统进入休眠状态。一个continuation对象包含在我们获得它的地方重新启动程序的必要信息。操作系统做线程间的上下文切换时也是如此。唯一的区别是它仍然继续保持着控制权。如果你需要一个continuation对象(在Scheme中,可以调用call-with-current-continuation函数),你就会得到一个包含current continuation的对象,即栈或者是在CPS中下一个要调用的函数)。你可以将这个对象存到变量中(或者磁盘上)。当你用这个continuation对象“重启”程序的时候,就可以将程序“转换”到你取得这个对象时的那个状态。这和切换回一个被挂起的线程或者唤醒休眠着的操作系统是一回事,而且你可以一遍又一遍的这样做。当操作系统被唤醒时,休眠信息就被销毁了。但如果那些信息没有被销毁,那么你也可以从同一个点一次又一次的唤醒操作系统。这就像时间停止一样。使用continuation,你就有了这个控制力!

Continuation应该在什么情况下使用呢?一般是在我们希望在一个先天就无状态的应用中模拟状态的时候。这样可以简化任务。Continuation很适合在Web应用程序中使用。微软的ASP.NET花了很大的功夫来模拟状态,以便在开发Web应用时少费周折。如果C#支持continuation,ASP.NET就不那么复杂了——你只需要保存一个continuation,当用户再次发送web请求时,重新启动它就可以了。对于程序员来说,web应用程序将不再有中断——程序只是简单的从下一行开始执行就可以了!

对于一些问题来说,continuation是一个非常有用的抽象工具。想到很多传统复杂的客户端将走向网络,continuation在未来会变得越来越重要。

模式匹配

模式匹配不是什么新特性。事实上,它和函数式编程的关系不大。为什么总是把它当做函数式编程的一个特性呢?这是因为函数式语言已经支持模式匹配一段时间了,而现代命令式语言还不行。

用一个例子来进一步了解模式匹配。下面是Java实现的斐波那契函数:

1
2
3
4
5
6
int fib(int n) {
  if(n == 0) return 1;
  if(n == 1) return 1;
  return fib(n - 2) + fib(n - 1);
}

如果用我们上文构造的并且支持模式匹配的Java来写,实现如下

1
2
3
4
5
6
7
8
9
10
11
int fib(0) {
  return 1;
}
int fib(1) {
  return 1;
}
int fib(int n) {
  return fib(n - 2) + fib(n - 1);
}

两者有什么区别?编译器为我们实现了分支。

这有什么大不了的?的确没什么。有人注意到很多函数都包含非常复杂的switch语句(函数式程序中尤其如此)并且觉得这是一种很好的抽象方式。我们将函数拆分成多个,然后通过函数参数实现模式(有点象重载)。当编译器调用函数的时候,会比较传入的参数和定义中的参数,然后选择匹配的那个执行。通常来说,编译器会选择最佳匹配。比如,int fib(int n)也可以在当n为1时被匹配,但因为int fib(1)是最佳匹配,所以编译器不会选择int fib(int n)

模式匹配通常比例子中揭示的更加复杂。比如,高级模式匹配系统允许我们这样做:

1
2
int f(int n < 10) { ... }
int f(int n) { ... }

模式匹配在什么时候适用?当有一大堆case的时候!每次当你需要写一个包含嵌套if的复杂结构时,模式匹配都可以用更少的代码取得更好的效果。一个很好的例子闪现在我脑海里。这就是所有Win32平台都提供的标准WinProc函数(即使它通常被抽象了)。一般来说一个好的模式匹配系统既可以检查集合,也可以检查简单值。比如,当传给函数一个数组后,可以设计这样一个模式:当首元素为1并且第三个元素大于3的时候,该模式被匹配。

模式匹配的另一个好处就是,当你需要添加或者改变条件时,不用去检查这个庞大的函数。而只需要添加(或修改)相应的那个模式定义。这样Gof书上的一大部分设计模式就没用了。条件越复杂,模式匹配就越有用。一旦你熟悉了模式匹配,就会开始奇怪:没有它的这些年你是怎么挨过来的。

闭包(Closure)

我们已经讨论了纯函数式语言的很多功能——所谓“纯”函数式语言就是实现了lambda演算并且不包含与Church范式矛盾的特性。但是函数式语言并不仅限于lambda演算。虽然实现一个自我证明的系统非常有用,它可以让我们以数学的方式来思考程序,但是可能在实际中它没有什么用处。所以很多语言选择支持部分函数式元素,但又不严格遵守那些教条。一些语言(比如Common Lisp)不要求变量是final的。你可以随时修改变量。它们的函数也不仅依赖于函数的参数。函数可以访问外部状态。但这些语言的确包含了函数式特性,比如高阶函数。在非纯粹的函数式语言里以函数作为参数传递,和在lambda演算系统中有些不同,它需要一种被称为词法闭包(lexical closure)的有趣特性。让我们来看看这段例子代码。记住,这回变量不是final的,并且函数可以引用其作用域外的变量:

1
2
3
4
5
6
7
8
9
Function makePowerFn(int power) {
  int powerFn(int base) {
    return pow(base, power);
  }
  return powerFn;
}
Function square = makePowerFn(2);
square(3); // returns 9

函数makePowerFn返回了一个函数。这个函数有一个参数,并对这个参数进行幂运算。如果我们对square(3)求值会发生什么?变量power其实已经不在powerFn的作用域中了,因为makePowerFn已经返回,所以它的栈已经消失了。那么square是怎么执行的?一定是这个语言以某种方式将power的值保存起来以便square使用。如果我们创建另一个函数cube,为参数的立方运算会怎么样?运行环境必须存储两个power,每个我们用makePowerFn生成的函数(square和cube)各使用一个。存储这些值的现象就叫做闭包。闭包不只保存宿主函数的参数。例如,closure可能会是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Function makeIncrementer() {
  int n = 0;
  int increment() {
    return ++n;
  }
}
Function inc1 = makeIncrementer();
Function inc2 = makeIncrementer();
inc1(); // returns 1;
inc1(); // returns 2;
inc1(); // returns 3;
inc2(); // returns 1;
inc2(); // returns 2;
inc2(); // returns 3;

运行时已经保存了n,所以递增器可以访问它。更重要的是,运行时为每个递增器都保存了一个n的拷贝,即使这些拷贝应该在makeIncrementer返回时消失。那么这些代码是被如何编译的?闭包运算又是怎么工作的?让我们去幕后看看。

一点常识会很有帮助,首先局部变量不再由简单的作用域规则限定,它们的生命周期也不确定。那么由此可以得出它们不再被保存在栈上,而是堆中·8(#note8)。这样一来,闭包的实现就与我们前面讨论的函数一样了,只是它还有一个指向周围变量的引用。

1
2
3
4
5
class some_function_t {
  SymbolTable parentScope;
  // ...
}

当闭包引用了一个不在其作用域的变量时,它便会查找其父作用域中的引用。这样闭包将函数式和面向对象程序设计相结合。每当你创建一个包含了状态的类,并且把它传到别处的时候,想想闭包吧。闭包就是一个可以在运行时创建并获取“成员变量”的对象,只是你不必亲自去做这些!

接下来如何?

这篇文章仅介绍了函数式程序设计的一些皮毛。即是所谓的抛砖引玉吧。未来我打算写一写关于分类理论(category theory),单一体(monad),函数数据结构(functional data structure),函数式程序设计中的类型系统,FP并发,函数式数据库等等。如果我能(在学习的过程中)写出这些主题的一半,我想我的人生就完整了。与此同时,Google是我们的好朋友。

意见或建议?

如果你有任何问题、意见、或建议,请发送邮件至coffeemug@gmail.com。我非常愿意听到您的反馈。


  1. 当我2005年秋天找工作的时候,我真的常常问这个问题。让人惊讶的是,我看到了很多茫然的面孔。你得想想,这帮年薪30万美金的人对于大部分他们能接触到的工具都有一个很好的理解。
  2. 这是一个有争议的问题。物理学家和数学家一直被迫承认,他们还不完全清楚宇宙万物所遵循的规则是否都可以被数学描述。
  3. 我非常讨厌那种仅给出一串日期、名字、事件的历史课。对我来说,历史是那些改变历史的人的生活,是他们行动背后的动机,是他们影响世人的方式。因此,我写的这一节并不能涵盖所有的历史细节。我只介绍那些与函数式程序设计相关的人和事。
  4. 我在学习函数式程序设计的时候,很不喜欢术语lambda,因为我搞不清楚它到底是什么意思。本文中,lambda就是一个函数,一个方便使用的希腊字母。当谈到函数式编程的时候,如果你听到“lambda”,在脑子里把它翻译成“函数”就行了。
  5. 有趣的是,Java中的字符串就是不可变的。讨论为什么会出现这样离经叛道的设计可能更有趣,但是这会打断我们当前的话题。
  6. 几乎所有的函数式语言编译器都会尽可能的将递归优化为迭代。这中优化被称为尾递归优化(tail call optimization)。
  7. 相反的情况未必成立。尽管有时可以证明两段代码等价,但无法找到一种普世方法可以证明所有情况。
  8. 这其实不比存在栈中慢,因为如果你引入了垃圾回收器,那么内存分配便成为了一个O(1)操作。

from :http://blog.pengyifan.com/articles/functional-programming-for-the-rest-of-us/

 

函数式编程

当我们说起函数式编程来说,我们会看到如下函数式编程的长相:

  • 函数式编程的三大特性:
    • immutable data 不可变数据:像Clojure一样,默认上变量是不可变的,如果你要改变变量,你需要把变量copy出去修改。这样一来,可以让你的程序少很多Bug。因为,程序中的状态不好维护,在并发的时候更不好维护。(你可以试想一下如果你的程序有个复杂的状态,当以后别人改你代码的时候,是很容易出bug的,在并行中这样的问题就更多了)
    • first class functions:这个技术可以让你的函数就像变量一样来使用。也就是说,你的函数可以像变量一样被创建,修改,并当成变量一样传递,返回或是在函数中嵌套函数。这个有点像Javascript的Prototype(参看Javascript的面向对象编程
    • 尾递归优化:我们知道递归的害处,那就是如果递归很深的话,stack受不了,并会导致性能大幅度下降。所以,我们使用尾递归优化技术——每次递归时都会重用stack,这样一来能够提升性能,当然,这需要语言或编译器的支持。Python就不支持。
  • 函数式编程的几个技术
    • map & reduce:这个技术不用多说了,函数式编程最常见的技术就是对一个集合做Map和Reduce操作。这比起过程式的语言来说,在代码上要更容易阅读。(传统过程式的语言需要使用for/while循环,然后在各种变量中把数据倒过来倒过去的)这个很像C++中的STL中的foreach,find_if,count_if之流的函数的玩法。
    • pipeline:这个技术的意思是,把函数实例成一个一个的action,然后,把一组action放到一个数组或是列表中,然后把数据传给这个action list,数据就像一个pipeline一样顺序地被各个函数所操作,最终得到我们想要的结果。
    • recursing 递归:递归最大的好处就简化代码,他可以把一个复杂的问题用很简单的代码描述出来。注意:递归的精髓是描述问题,而这正是函数式编程的精髓。
    • currying:把一个函数的多个参数分解成多个函数, 然后把函数多层封装起来,每层函数都返回一个函数去接收下一个参数这样,可以简化函数的多个参数。在C++中,这个很像STL中的bind_1st或是bind2nd。
    • higher order function 高阶函数:所谓高阶函数就是函数当参数,把传入的函数做一个封装,然后返回这个封装函数。现象上就是函数传进传出,就像面向对象对象满天飞一样。

 

  • 还有函数式的一些好处
    • parallelization 并行:所谓并行的意思就是在并行环境下,各个线程之间不需要同步或互斥。
    • lazy evaluation 惰性求值:这个需要编译器的支持。表达式不在它被绑定到变量之后就立即求值,而是在该值被取用的时候求值,也就是说,语句如x:=expression; (把一个表达式的结果赋值给一个变量)明显的调用这个表达式被计算并把结果放置到 x 中,但是先不管实际在 x 中的是什么,直到通过后面的表达式中到 x 的引用而有了对它的值的需求的时候,而后面表达式自身的求值也可以被延迟,最终为了生成让外界看到的某个符号而计算这个快速增长的依赖树。
    • determinism 确定性:所谓确定性的意思就是像数学那样 f(x) = y ,这个函数无论在什么场景下,都会得到同样的结果,这个我们称之为函数的确定性。而不是像程序中的很多函数那样,同一个参数,却会在不同的场景下计算出不同的结果。所谓不同的场景的意思就是我们的函数会根据一些运行中的状态信息的不同而发生变化。

上面的那些东西太抽象了,还是让我们来循序渐近地看一些例子吧。

我们先用一个最简单的例子来说明一下什么是函数式编程。

先看一个非函数式的例子:

1
2
3
4
int cnt; void increment(){
    cnt++;
}

那么,函数式的应该怎么写呢?

1
2
3
int increment(int cnt){
    return cnt+1;
}

你可能会觉得这个例子太普通了。是的,这个例子就是函数式编程的准则:不依赖于外部的数据,而且也不改变外部数据的值,而是返回一个新的值给你

我们再来看一个简单例子:

1
2
3
4
5
6
7
8
9
10
def inc(x):
    def incx(y):
        return x+y
    return incx
 
inc2 = inc(2)
inc5 = inc(5)
 print inc2(5) # 输出 7
print inc5(5) # 输出 10

我们可以看到上面那个例子inc()函数返回了另一个函数incx(),于是我们可以用inc()函数来构造各种版本的inc函数,比如:inc2()和inc5()。这个技术其实就是上面所说的Currying技术。从这个技术上,你可能体会到函数式编程的理念:把函数当成变量来用,关注于描述问题而不是怎么实现,这样可以让代码更易读。

Map & Reduce

在函数式编程中,我们不应该用循环迭代的方式,我们应该用更为高级的方法,如下所示的Python代码

1
2
3
name_len = map(len, ["hao", "chen", "coolshell"]) print name_len # 输出 [3, 4, 9]

你可以看到这样的代码很易读,因为,这样的代码是在描述要干什么,而不是怎么干

我们再来看一个Python代码的例子:

1
2
3
4
5
6
def toUpper(item):
      return item.upper()
 
upper_name = map(toUpper, ["hao", "chen", "coolshell"]) print upper_name # 输出 ['HAO', 'CHEN', 'COOLSHELL']

顺便说一下,上面的例子个是不是和我们的STL的transform有些像?

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <algorithm>
#include <string>
using namespace std;
 int main() {
  string s="hello";
  string out;
  transform(s.begin(), s.end(), back_inserter(out), ::toupper);
  cout << out << endl;
  // 输出:HELLO }

在上面Python的那个例子中我们可以看到,我们写义了一个函数toUpper,这个函数没有改变传进来的值,只是把传进来的值做个简单的操作,然后返回。然后,我们把其用在map函数中,就可以很清楚地描述出我们想要干什么。而不会去理解一个在循环中的怎么实现的代码,最终在读了很多循环的逻辑后才发现原来是这个或那个意思。 下面,我们看看描述实现方法的过程式编程是怎么玩的(看上去是不是不如函数式的清晰?):

1
2
3
4
upname =['HAO', 'CHEN', 'COOLSHELL']
lowname =[]  for i in range(len(upname)):
    lowname.append( upname[i].lower() )

对于map我们别忘了lambda表达式:你可以简单地理解为这是一个inline的匿名函数。下面的lambda表达式相当于:def func(x): return x*x

1
2
3
squares = map(lambda x: x * x, range(9)) print squares # 输出 [0, 1, 4, 9, 16, 25, 36, 49, 64]

我们再来看看reduce怎么玩?(下面的lambda表达式中有两个参数,也就是说每次从列表中取两个值,计算结果后把这个值再放回去,下面的表达式相当于:((((1+2)+3)+4)+5) )

1
2
print reduce(lambda x, y: x+y, [1, 2, 3, 4, 5]) # 输出 15

Python中的除了map和reduce外,还有一些别的如filter, find, all, any的函数做辅助(其它函数式的语言也有),可以让你的代码更简洁,更易读。 我们再来看一个比较复杂的例子:

计算数组中正数的平均值
1
2
3
4
5
6
7
8
9
10
11
12
13
num =[2, -5, 9, 7, -2, 5, 3, 1, 0, -3, 8]
positive_num_cnt = 0
positive_num_sum = 0 for i in range(len(num)):
    if num[i] > 0:
        positive_num_cnt += 1
        positive_num_sum += num[i]
 if positive_num_cnt > 0:
    average = positive_num_sum / positive_num_cnt
 print average # 输出 5

如果用函数式编程,这个例子可以写成这样:

1
2
positive_num = filter(lambda x: x>0, num)
average = reduce(lambda x,y: x+y, positive_num) / len( positive_num )

C++11玩的法:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <algorithm>
#include <numeric>
#include <string>
#include <vector>
using namespace std;
 
vector num {2, -5, 9, 7, -2, 5, 3, 1, 0, -3, 8};
vector p_num;
copy_if(num.begin(), num.end(), back_inserter(p_num), [](int i){ return (i>0);} ); int average = accumulate(p_num.begin(), p_num.end(), 0) / p_num.size();
cout << "averge: " << average << endl;

我们可以看到,函数式编程有如下好处:

1)代码更简单了。 2)数据集,操作,返回值都放到了一起。 3)你在读代码的时候,没有了循环体,于是就可以少了些临时变量,以及变量倒来倒去逻辑。 4)你的代码变成了在描述你要干什么,而不是怎么去干。

最后,我们来看一下Map/Reduce这样的函数是怎么来实现的(下面是Javascript代码)

map函数
1
2
3
4
5
6
7
var map = function (mappingFunction, list) {
  var result = [];
  forEach(list, function (item) {
    result.push(mappingFunction(item));
  });
  return result;
};

下面是reduce函数的javascript实现(谢谢 @下雨在家 修正的我原来的简单版本)

reduce函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function reduce(actionFunction, list, initial){
    var accumulate;
    var temp;
    if(initial){
        accumulate = initial;
    }
    else{
        accumulate = list.shfit();
    }
    temp = list.shift();
    while(temp){
        accumulate = actionFunction(accumulate,temp);
        temp = list.shift();
    }
    return accumulate;
};

Declarative Programming vs Imperative Programming

前面提到过多次的函数式编程关注的是:describe what to do, rather than how to do it. 于是,我们把以前的过程式的编程范式叫做 Imperative Programming – 指令式编程,而把函数式的这种范式叫做 Declarative Programming – 声明式编程。

下面我们看一下相关的示例(本示例来自这篇文章 )。

比如,我们有3辆车比赛,简单起见,我们分别给这3辆车有70%的概率可以往前走一步,一共有5次机会,我们打出每一次这3辆车的前行状态。

对于Imperative Programming来说,代码如下(Python):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from random import random
 
time = 5
car_positions = [1, 1, 1]
 while time:
    # decrease time     time -= 1
 
    print ''
    for i in range(len(car_positions)):
        # move car         if random() > 0.3:
            car_positions[i] += 1
 
        # draw car         print '-' * car_positions[i]

我们可以把这个两重循环变成一些函数模块,这样有利于我们更容易地阅读代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from random import random
 def move_cars():
    for i, _ in enumerate(car_positions):
        if random() > 0.3:
            car_positions[i] += 1
 def draw_car(car_position):
    print '-' * car_position
 def run_step_of_race():
    global time
    time -= 1
    move_cars()
 def draw():
    print ''
    for car_position in car_positions:
        draw_car(car_position)
 
time = 5
car_positions = [1, 1, 1]
 while time:
    run_step_of_race()
    draw()

上面的代码,我们可以从主循环开始,我们可以很清楚地看到程序的主干,因为我们把程序的逻辑分成了几个函数,这样一来,我们的代码逻辑也会变得几个小碎片,于是我们读代码时要考虑的上下文就少了很多,阅读代码也会更容易。不像第一个示例,如果没有注释和说明,你还是需要花些时间理解一下。而把代码逻辑封装成了函数后,我们就相当于给每个相对独立的程序逻辑取了个名字,于是代码成了自解释的

但是,你会发现,封装成函数后,这些函数都会依赖于共享的变量来同步其状态。于是,我们在读代码的过程时,每当我们进入到函数里,一量读到访问了一个外部的变量,我们马上要去查看这个变量的上下文,然后还要在大脑里推演这个变量的状态, 我们才知道程序的真正逻辑。也就是说,这些函数间必需知道其它函数是怎么修改它们之间的共享变量的,所以,这些函数是有状态的

我们知道,有状态并不是一件很好的事情,无论是对代码重用,还是对代码的并行来说,都是有副作用的。因此,我们要想个方法把这些状态搞掉,于是出现了我们的 Functional Programming 的编程范式。下面,我们来看看函数式的方式应该怎么写?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from random import random
 def move_cars(car_positions):
    return map(lambda x: x + 1 if random() > 0.3 else x,
               car_positions)
 def output_car(car_position):
    return '-' * car_position
 def run_step_of_race(state):
    return {'time': state['time'] - 1,
            'car_positions': move_cars(state['car_positions'])}
 def draw(state):
    print ''
    print '\n'.join(map(output_car, state['car_positions']))
 def race(state):
    draw(state)
    if state['time']:
        race(run_step_of_race(state))
 
race({'time': 5,
      'car_positions': [1, 1, 1]})

上面的代码依然把程序的逻辑分成了函数,不过这些函数都是functional的。因为它们有三个症状:

1)它们之间没有共享的变量。 2)函数间通过参数和返回值来传递数据。 3)在函数里没有临时变量。

我们还可以看到,for循环被递归取代了(见race函数)—— 递归是函数式编程中带用到的技术,正如前面所说的,递归的本质就是描述问题是什么。

Pipeline

pipeline 管道借鉴于Unix Shell的管道操作——把若干个命令串起来,前面命令的输出成为后面命令的输入,如此完成一个流式计算。(注:管道绝对是一个伟大的发明,他的设哲学就是KISS – 让每个功能就做一件事,并把这件事做到极致,软件或程序的拼装会变得更为简单和直观。这个设计理念影响非常深远,包括今天的Web Service,云计算,以及大数据的流式计算等等)

比如,我们如下的shell命令:

1
ps auwwx | awk '{print $2}' | sort -n | xargs echo

如果我们抽象成函数式的语言,就像下面这样:

1
xargs(  echo, sort(n, awk('print $2', ps(auwwx)))  )

也可以类似下面这个样子:

1
pids = for_each(result, [ps_auwwx, awk_p2, sort_n, xargs_echo])

好了,让我们来看看函数式编程的Pipeline怎么玩?

我们先来看一个如下的程序,这个程序的process()有三个步骤:

1)找出偶数。 2)乘以3 3)转成字符串返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def process(num):
    # filter out non-evens     if num % 2 != 0:
        return
    num = num * 3
    num = 'The Number: %s' % num
    return num
 
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
 for num in nums:
    print process(num)
 # 输出:
# None
# The Number: 6
# None
# The Number: 12
# None
# The Number: 18
# None
# The Number: 24
# None
# The Number: 30

我们可以看到,输出的并不够完美,另外,代码阅读上如果没有注释,你也会比较晕。下面,我们来看看函数式的pipeline(第一种方式)应该怎么写?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def even_filter(nums):
    for num in nums:
        if num % 2 == 0:
            yield num def multiply_by_three(nums):
    for num in nums:
        yield num * 3 def convert_to_string(nums):
    for num in nums:
        yield 'The Number: %s' % num
 
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
pipeline = convert_to_string(multiply_by_three(even_filter(nums))) for num in pipeline:
    print num # 输出:
# The Number: 6
# The Number: 12
# The Number: 18
# The Number: 24
# The Number: 30

我们动用了Python的关键字 yield,这个关键字主要是返回一个Generator,yield 是一个类似 return 的关键字,只是这个函数返回的是个Generator-生成器。所谓生成器的意思是,yield返回的是一个可迭代的对象,并没有真正的执行函数。也就是说,只有其返回的迭代对象被真正迭代时,yield函数才会正真的运行,运行到yield语句时就会停住,然后等下一次的迭代。(这个是个比较诡异的关键字)这就是lazy evluation。

好了,根据前面的原则——“使用Map & Reduce,不要使用循环”,那我们用比较纯朴的Map & Reduce吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def even_filter(nums):
    return filter(lambda x: x%2==0, nums)
 def multiply_by_three(nums):
    return map(lambda x: x*3, nums)
 def convert_to_string(nums):
    return map(lambda x: 'The Number: %s' % x,  nums)
 
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
pipeline = convert_to_string(
               multiply_by_three(
                   even_filter(nums)
               )
            ) for num in pipeline:
    print num

但是他们的代码需要嵌套使用函数,这个有点不爽,如果我们能像下面这个样子就好了(第二种方式)。

1
2
3
pipeline_func(nums, [even_filter,
                     multiply_by_three,
                     convert_to_string])

那么,pipeline_func 实现如下:

1
2
3
4
def pipeline_func(data, fns):
    return reduce(lambda a, x: x(a),
                  fns,
                  data)

好了,在读过这么多的程序后,你可以回头看一下这篇文章的开头对函数式编程的描述,可能你就更有感觉了。

最后,我希望这篇浅显易懂的文章能让你感受到函数式编程的思想,就像OO编程,泛型编程,过程式编程一样,我们不用太纠结是不是我们的程序就是OO,就是functional的,我们重要的品味其中的味道

参考

补充:评论中redraiment这个评论大家也可以读一读。

感谢谢网友S142857 提供的shell风格的python pipeline:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Pipe(object):
    def __init__(self, func):
        self.func = func
 
    def __ror__(self, other):
        def generator():
            for obj in other:
                if obj is not None:
                    yield self.func(obj)
        return generator()
@Pipe def even_filter(num):
    return num if num % 2 == 0 else None
@Pipe def multiply_by_three(num):
    return num*3
@Pipe def convert_to_string(num):
    return 'The Number: %s' % num
@Pipe def echo(item):
    print item
    return item
 def force(sqs):
    for item in sqs: pass
 
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
 
force(nums | even_filter | multiply_by_three | convert_to_string | echo)

(全文完)
from:http://coolshell.cn/articles/10822.html 酷 壳 – CoolShell.cn

Free Web Hosting