关于函数式编程

函数式编程与面向对象编程和面向过程编程一样,属于一种编程范式。

关于后两者,这里贴一个知乎回答,大家自己体会:

面向对象是相对于面向过程的,比如你要充话费,你会想,可以下个支付宝,然后绑定银行卡,然后在淘宝上买卡,自己冲,这种种过程。但是对于你女朋友就不一样了,她是面向“对象”的,她会想,谁会充话费呢?当然是你了,她就给你电话,然后你把之前的做了一遍,然后她收到到帐的短信,说了句,亲爱的。这就是面向对象!女的思维大部分是面向“对象”的!她不关心处理的细节,只关心谁可以,和结果!


作者:知乎用户
链接:https://www.zhihu.com/question/31021366/answer/50581592
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 

那么大名鼎鼎的函数式编程又是怎么回事呢?

定义不好下,我们可以通过几个特点来管中窥豹,略见一斑^_^

函数式编程的几个特点

1. 以函数作为主要载体。

函数地位最高,函数可以赋值给一个变量,函数可以作为参数,函数还可以作为返回值。

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

"表达式"是一个单纯的运算过程,总是有返回值;"语句"(statement)是执行某种操作,没有返回值。

函数式编程的开发动机,一开始就是为了处理运算,不考虑系统的读写(I/O)。"语句"属于对系统的读写操作,所以函数式编程排斥使用语句。

当然,实际应用中,不做I/O是不可能的。因此,编程过程中,函数式编程只要求把I/O限制到最小,不要有不必要的读写行为,保持计算过程的单纯性。

 3. 没有副作用。

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

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

不修改变量,意味着状态不能保存在变量中。函数式编程使用参数保存状态,最好的例子就是递归。

4. 引用透明。

引用透明,指的是函数的运行不依赖于外部变量或"状态",只依赖于输入的参数,任何时候只要参数相同,引用函数所得到的返回值总是相同的。

深入探究函数式编程

一、高阶函数

二、函数的合成与柯里化

函数式编程有两个最基本的运算:合成和柯里化。

1. 合成

合成像一系列管道那样把不同的函数联系在一起。例如:

// f 和 g 都是函数,x 是在它们之间通过“管道”传输的值。
const compose = function (f, g) { 
  return function (x) { 
    return f(g(x)) 
  } 
}


const toUpperCase = function (x) { return x.toUpperCase() }
const exclaim = function (x) { return `${x}!` }

const shout = compose(toUpperCase, exclaim)   // 调用 compose 返回一个新函数
shout('functional programing is useful')  // "FUNCTIONAL PROGRAMING IS USEFUL!"

2. 柯里化

所谓"柯里化",就是把一个多参数的函数,转化为单参数函数。

// 柯里化之前
function add(x, y) {
  return x + y;
}

add(1, 2) // 3

// 柯里化之后
function addX(y) {
  return function (x) {
    return x + y;
  };
}

addX(2)(1) // 3

三、递归和尾递归

http://www.ruanyifeng.com/blog/2015/04/tail-call.html

1 函子

1.1 概念

函子是函数式编程里面最重要的数据类型,也是基本的运算单位和功能单位。

它是一个容器,包含了值和变形关系。比较特殊的是,它的变形关系可以依次作用于每一个值,将当前容器变形成另一个容器。

上图中,左侧的圆圈就是一个函子,表示人名的范畴。外部传入函数f,会转成右边表示早餐的范畴。

1.2 函子的代码实现

任何具有map方法的数据结构,都可以当作函子的实现。

class Functor {
  constructor (val) {
    this.val = val
  }

  map (f) {
    return new Functor(f(this.val))
  }
}

(new Functor(2)).map(x => x + 10)  // Functor(12)

上面的例子说明,函数式编程里面的运算,都是通过函子完成,即运算不直接针对值,而是针对这个值的容器----函子。函子本身具有对外接口(map方法),各种函数就是运算符,通过接口接入容器,引发容器里面的值的变形。

因此,学习函数式编程,实际上就是学习函子的各种运算。由于可以把运算方法封装在函子里面,所以又衍生出各种不同类型的函子,有多少种运算,就有多少种函子。函数式编程就变成了运用不同的函子,解决实际问题。

1.3 函子的 of 方法

上面生成新的函子的时候,用了new命令。这实在太不像函数式编程了,因为new命令是面向对象编程的标志。

函数式编程一般约定,函子有一个of方法,用来生成新的容器。

使用 static 关键字给 Functor 添加一个静态方法 of ,使得 of 可以直接在 Functor类 上调用,而不是在 Functor类 的实例上调用。

class Functor {
  constructor (val) {
    this.val = val
  }

  static of (val) {
    return new Functor(val)
  }

  map (f) {
    return new Functor(f(this.val))
  }
}

Functor.of(2).map(x => x + 10) // Functor(12)
Functor.of(2).map(x => x + 10).map(x => x + 100) // Functor(112)

2 Maybe 函子

函子接受各种函数,处理容器内部的值。这里就有一个问题,容器内部的值可能是一个空值(比如null),而外部函数未必有处理空值的机制,如果传入空值,很可能就会出错。

Maybe 函子就是为了解决这一类问题而设计的。简单说,它的map方法里面设置了空值检查。

// Maybe 函子
class Maybe {
  constructor (val) {
    this.val = val
  }

  static of (val) {
    return new Maybe(val)
  }

  map (f) {
    return this.val ? Maybe.of(f(this.val)) : Maybe.of(null)
  }
}

Maybe.of(null).map(s => s.toUpperCase()) // Maybe(null)

3 Either 函子

条件运算if...else是最常见的运算之一,函数式编程里面,使用 Either 函子表达。

Either 函子内部有两个值:左值(Left)和右值(Right)。右值是正常情况下使用的值,右值不存在时使用左侧默认值。

// Either 函子
class Either {
  constructor (left, right) {
    this.left = left
    this.right = right
  }

  static of (left, right) {
    return new Either(left, right)
  }

  map (f) {
    return this.right
      ? Either.of(this.left, f(this.right))
      : Either.of(f(this.left), this.right)
  }
}

Either.of(5, 6).map(x => x + 1) // Either(5, 7)
Either.of(1, null).map(x => x + 1) // Either(2, null)

4 ap 函子

一个函子的值是数值,另一个函子的值是函数。下面代码中,函子A内部的值是2,函子B内部的值是函数addTwo

function addTwo(x) {
  return x + 2;
}

const A = Functor.of(2);
const B = Functor.of(addTwo)

有时,我们想让函子B内部的函数,可以使用函子A内部的值进行运算。这时就需要用到 ap 函子。

ap 是 applicative(应用)的缩写。凡是部署了ap方法的函子,就是 ap 函子。

class Functor {
  constructor (val) {
    this.val = val
  }

  static of (val) {
    return new Functor(val)
  }

  map (f) {
    return new Functor(f(this.val))
  }
}

// ap函子
class Ap {
  constructor (val) {
    this.val = val
  }

  static of (val) {
    return new Ap(val)
  }

  ap (F) {
    return Ap.of(this.val(F.val))
  }
}

function addTwo (x) {
  return x + 2
}

Ap.of(addTwo).ap(Functor.of(9)) // Ap(11)

注意,ap方法的参数不是函数,而是另一个函子。

ap 函子的意义在于,对于那些多参数的函数,就可以从多个容器之中取值,实现函子的链式操作。

// Maybe 函子
class Maybe {
  constructor (val) {
    this.val = val
  }

  static of (val) {
    return new Maybe(val)
  }

  map (f) {
    return this.val ? Maybe.of(f(this.val)) : Maybe.of(null)
  }
}

// ap函子
class Ap {
  constructor (val) {
    this.val = val
  }

  static of (val) {
    return new Ap(val)
  }

  ap (F) {
    return Ap.of(this.val(F.val))
  }
}

function add (x) {
  return function (y) {
    return x + y
  }
}

Ap.of(add).ap(Maybe.of(2)).ap(Maybe.of(3)) // Ap(5)

上面代码中,函数add是柯里化以后的形式,一共需要两个参数。通过 ap 函子,我们就可以实现从两个容器之中取值。它还有另外一种调用写法。

Ap.of(add(2)).ap(Maybe.of(3)) // Ap(5)

5 Monad 函子

 函子是一个容器,可以包含任何值。函子之中再包含一个函子,也是完全合法的。但是,这样就会出现多层嵌套的函子。

Maybe.of(
  Maybe.of(
    Maybe.of({name: 'Mulburry', number: 8402})
  )
)

 

上面这个函子,一共有三个Maybe嵌套。如果要取出内部的值,就要连续取三次this.val。这当然很不方便,因此就出现了 Monad 函子。

Monad 函子的作用是,总是返回一个单层的函子。它有一个flatMap方法,与map方法作用相同,唯一的区别是如果生成了一个嵌套函子,它会取出后者内部的值,保证返回的永远是一个单层的容器,不会出现嵌套的情况。

// Monad函子
class Monad {
  constructor (val) {
    this.val = val
  }

  static of (val) {
    return new Monad(val)
  }

  join () {
    return this.val
  }

  flatMap (f) {
    return this.map(f).join()
  }

  map (f) {
    return new Monad(f(this.val))
  }
}

function monadTest () {
  return Functor.of(3)
}

Monad.flatMap(monadTest) // Functor(3)

上面代码中,函数f返回的是一个函子,那么this.map(f)就会生成一个嵌套的函子。所以,join方法保证了flatMap方法总是返回一个单层的函子。这意味着嵌套的函子会被铺平(flatten)。

6 IO 操作

Monad 函子的重要应用,就是实现 I/O (输入输出)操作。

I/O 是不纯的操作,普通的函数式编程没法做,这时就需要把 IO 操作写成Monad函子,通过它来完成。

class IO {
  constructor(val) {
    this.val = val
  }

  static of(val) {
    return new IO(val)
  }

  join() {
    return this.val
  }

  flatMap(f) {
    return this.map(f).join()
  }

  map(f) {
    return new IO(f(this.val))
  }

  run() {
    debugger
    if (typeof this.val === 'function') this.val()
    else console.log(this.val);
  }
}

const a = function (filename) {
  function a1() {
    setTimeout(() => {
      console.log(filename)
    }, 500);
  }
  return a1
}

const b = function (x) {
  function b1() {
    if (typeof x === 'function') x()
    else console.log(x)
    return x;
  }
  return b1
}

const c = function (x) {
  function c1() {
    if (typeof x === 'function') x()
    else console.log(x)
    return x;
  }

  return c1
}

const readFile = function (filename) {
  return new IO(a(filename));
};

const print = function (x) {
  return new IO(b(x));
}

const tail = function (x) {
  return new IO(c(x));
}

readFile('a-file-name')
  .flatMap(print)
  .flatMap(tail)
  .run()
// 最终会执行函数a1, 打印出'a-file-name'

上面的代码完成了不纯的操作,但是因为flatMap返回的还是一个 IO 函子,所以这个表达式是纯的。我们通过一个纯的表达式,完成带有副作用的操作,这就是 Monad 的作用。

由于返回还是 IO 函子,所以可以实现链式操作。因此,在大多数库里面,flatMap方法被改名成chain

函数式编程库

posted on 2018-11-29 19:52  dawnxuuu  阅读(120)  评论(0编辑  收藏  举报

导航