JavaScript函数式编程

Functional Programming in JavaScript

by StEve Young

一、什么是函数式编程?

What is FP?

Functional programming is a programming paradigm

1.treats computation as the evaluation of mathematical functions

2.avoids changing-state and mutable data

- by wikipedia

1.1.什么是编程范式?

What is Programming Paradigm?

编程范式从概念上来讲指的是编程的基本风格和典范模式。

——世界观和方法论

如果把一门编程语言比作兵器,它的语法、工具和技巧等是招法,那么它采用的编程范式也就是是内功心法。

1.2.什么是数学函数?

What is Mathematical Functions?

一般的,在一个变化过程中,假设有两个变量 x、y,如果对于任意一个 x 都有唯一确定的一个y和它对应,那么就称 x 是自变量,y 是 x 的函数。x 的取值范围叫做这个函数的定义域,相应 y 的取值范围叫做函数的值域

1.2.1.什么是纯函数?

What is Pure Functions?

纯函数(Pure Functions)是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。

引用透明性(Referential Transparency)指的是,如果一段代码在不改变整个程序行为的前提下,可以替换成它执行所得的结果。

const double = x => x * 2
const addFive = x => x + 5
const num = double(addFive(10))

num === double(10 + 5)
    === double(15)
    === 15 * 2
    === 30

1.2.2.什么是副作用?

What is Side Effects?

副作用是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互。包括但不限于:

  • 更改文件系统
  • 往数据库插入记录
  • 发送一个 http 请求
  • 改变数据
  • 打印/log
  • 获取用户输入
  • DOM 查询
  • 访问系统状态
  • ...
只要是跟函数外部环境发生的交互就都是副作用——这一点可能会让你怀疑无副作用编程的可行性。

函数式编程的哲学就是假定副作用是造成不正当行为的主要原因。

1.2.3.纯函数的好处都有啥?

Why Pure Functions?

面向对象语言的问题是,它们永远都要随身携带那些隐式的环境。你只需要一个香蕉,但却得到一个拿着香蕉的大猩猩...以及整个丛林

  • 可缓存性(Cacheable)
  • 可测试性(Testable)
  • 合理性(Reasonable)
  • 并行代码(Parallel Code)
  • 可移植性/自文档化(Portable / Self-Documenting)

by  Erlang 作者:Joe Armstrong

const obj = { val: 1 }
someFn(obj)
console.log(obj) // ??

1.3.为什么要避免改变状态和可变数据?

Why avoid Changing-State and Mutable Data?

1.3.为什么要避免改变状态和可变数据?

Why avoid Changing-State and Mutable Data?

- from Shared mutable state B L

1.4.原教旨函数式 VS 温和派函数式?

Pure FP VS Impure FP?

  • Haskell
  • OCaml
  • Lisp
  • Erlang
  • Scala
  • F#
  • ...
  • JavaScript?
  • 函数是“一等公民”
  • 不可变数据
  • 使用递归而不是循环
  • 柯里化
  • 惰性求值
  • 代数数据类型
  • 模式匹配
  • ...

事实上,JavaScript 是一门基于原型(prototype-based)的多范式语言。

  • 高阶函数
  • 函数是一等公民
  • 闭包
  • 匿名函数
  • 箭头函数
  • 解构(模式匹配)
  • ...

1.4.原教旨函数式 VS 温和派函数式?

Pure FP VS Impure FP?

1.5. 作为函数式语言 JavaScript 还差什么?

The missing things from JS for a FP?

 

  1. 不可变数据结构
  2. 惰性求值
  3. 函数组合
  4. 尾递归优化
  5. 代数类型系统
wholeNameOf(
  getFirstName(), 
  getLastName()
)

[1, 2, 3, 4]
  .map(x => x + 1)

Lodash VS Underscore?

TypeScript?

二、声明式 VS 命令式

What VS How

Declarative VS Imperative

2.1.“意识形态”上的区别

The Difference In Ideology

声明式 程序抽象了控制流过程,代码描述的是 —— 数据流:即做什么。

命令式 代码描述用来达成期望结果的特定步骤 ——控制流:即如何做。

声明式 更多依赖表达式

 

表达式是指一小段代码,它用来计算某个值。表达式通常是某些函数调用的复合、一些值和操作符,用来计算出结果值。

命令式 频繁使用语句

 

语句是指一小段代码,它用来完成某个行为。通用的语句例子包括 forifswitchthrow,等等……

function mysteryFn(nums) {
  let squares = []
  let sum = 0

  for (let i = 0; i < nums.length; i++) {
    squares.push(nums[i] * nums[i])
  }
  
  for (let i = 0; i < squares.length; i++) {
    sum += squares[i]
  }

  return sum
}

1. 创建中间变量

2. 循环计算平方

3. 循环累加

以上代码都是 how 而不是 what...

const mysteryFn = (nums) => nums
  .map(x => x * x)
  .reduce((acc, cur) => acc + cur, 0)

a. 平方

2.2.举一些栗子🌰

b. 累加

Examples

例1:希望得到一个数组每个数据平方后的和

function mysteryFn(nums) {
  let sum = 0
  let tally = 0

  for (let i = 0; i < nums.length; i++) {
    if (nums[i] % 2 === 0) {
      sum += nums[i] / 2
      tally++
    }
  }

  return tally === 0 ? 0 : sum / tally
}

1. 创建中间变量

2. 循环,值为偶数时累加该值的一半并记录数量

3. 返回平均值

const mysteryFn = (nums) => nums
  .filter(x => x % 2 === 0)
  .map(x => x / 2)
  .reduce((acc, cur, idx, { length }) => (
    idx < length - 1
      ? acc + cur
      : (acc + cur) / length
  ), 0)

a. 过滤非偶数

b. 折半

c. 累加

d. 计算平均值

2.2.举一些栗子🌰

Examples

例2:希望得到一个数组所有偶数值的一半的平均值

三、可以,这很函数式~

OK, It's Very Functional

3.1.函数是一等公民!

First Class Function

// 太傻了
const getServerStuff = function (callback) {
  return ajaxCall(function (json) {
    return callback(json)
  })
}

// 这才像样
const getServerStuff = ajaxCall

// 下面来推导一下...
const getServerStuff
  === callback => ajaxCall(json => callback(json))
  === callback => ajaxCall(callback)
  === ajaxCall

// from JS函数式编程指南
  • 3.1.1.滥用匿名函数

3.1.函数是一等公民!

First Class Function

const BlogController = (function () {
  const index = function (posts) {
    return Views.index(posts)
  }

  const show = function (post) {
    return Views.show(post)
  }

  const create = function (attrs) {
    return Db.create(attrs)
  }

  const update = function (post, attrs) {
    return Db.update(post, attrs)
  }

  const destroy = function (post) {
    return Db.destroy(post)
  }

  return { index, show, create, update, destroy }
})()

// 以上代码 99% 都是多余的...
const BlogController = {
  index: Views.index,
  show: Views.show,
  create: Db.create,
  update: Db.update,
  destroy: Db.destroy,
}

// ...或者直接全部删掉
// 因为它的作用仅仅就是把视图(Views)
// 和数据库(Db)打包在一起而已。

// from JS函数式编程指南

3.1.函数是一等公民!

First Class Function

// 原始函数
httpGet('/post/2', function (json) {
  return renderPost(json)
})

// 假如需要多传递一个 err 参数
httpGet('/post/2', function (json, err) {
  return renderPost(json, err)
})

// renderPost 将会在 httpGet 中调用,
// 想要多少参数,想怎么改都行
httpGet('/post/2', renderPost)
  • 3.1.2.为何钟爱一等公民?

要同时修改两处

啥都不用传

3.1.函数是一等公民!

First Class Function

// 只针对当前的博客
const validArticles = function (articles) {
  return articles.filter(function (article) {
    return article !== null && article !== undefined
  })
}
  • 3.1.3.提高函数复用率
// 通用性好太多
const compact = function (xs) {
  return xs.filter(function (x) {
    return x !== null && x !== undefined
  })
}

3.1.函数是一等公民!

First Class Function

  • 3.1.4.this

在函数式编程中,其实根本用不到 this...

但这里并不是说要避免使用 this

江来报道上出了偏差...识得唔识得?

3.2.柯里化

Curry

import { curry } from 'lodash'

const add = (x, y) => x + y
const curriedAdd = curry(add)

const increment = curriedAdd(1)
const addTen = curriedAdd(10)

increment(2) // 3
addTen(2) // 12
  • 3.2.1.柯里化概念

把接受多个参数的函数变换成一系列接受单一参数(从最初函数的第一个参数开始)的函数的技术。(注意是单一参数)

柯里化是由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的

当然编程语言 Haskell 也是源自他的名字

虽然柯里化是由 Moses Schnfinkel 和 Gottlob Frege 发明的

3.2.柯里化

Curry

  • 3.2.2.柯里化 VS 偏函数应用(partial application)

In computer science, partial application (or partial function application) refers to the process of fixing a number of arguments to a function, producing another function of smaller arity.  -- by wikipedia

偏函数应用简单来说就是:一个函数,接受一个多参数的函数且传入部分参数后,返回一个需要更少参数的新函数。

import { curry, partial } from 'lodash'
const add = (x, y, z) => x + y + z

const curriedAdd = curry(add)       // <- 只接受一个函数
const addThree = partial(add, 1, 2) // <- 不仅接受函数,还接受至少一个参数
  === curriedAdd(1)(2)              // <- 柯里化每次都返回一个单参函数

一个多参函数(n-ary),柯里化后就变成了 n * 1-ary,而偏函数应用了 x 个参数后就变成了 (n-x)-ary

3.2.柯里化

Curry

  • 3.2.3.柯里化的实现
// 实现一个函数 curry 满足以下调用、
const f = (a, b, c, d) => { ... }
const curried = curry(f)

curried(a, b, c, d)
curried(a, b, c)(d)
curried(a)(b, c, d)
curried(a, b)(c, d)
curried(a)(b, c)(d)
curried(a)(b)(c, d)
curried(a, b)(c)(d)
// ES5
var curry = function curry (fn, arr) {
  arr = arr || []

  return function () {
    var args = [].slice.call(arguments)
    var arg = arr.concat(args)

    return arg.length >= fn.length
      ? fn.apply(null, arg)
      : curry(fn, arg)
  }
}
// ES6
const curry = (fn, arr = []) => (...args) => (
  arg => arg.length >= fn.length
    ? fn(...arg)
    : curry(fn, arg)
)([...arr, ...args])

递归

3.2.柯里化

Curry

  • 3.2.4.柯里化的意义
// 定义通用函数
const converter = (
  toUnit, 
  factor, 
  offset = 0, 
  input
) => ([
  ((offset + input) * factor).toFixed(2),
  toUnit,
].join(' '))

// 分别绑定不同参数
const milesToKm =
  curry(converter)('km', 1.60936, undefined)
const poundsToKg =
  curry(converter)('kg', 0.45460, undefined)
const farenheitToCelsius =
  curry(converter)('degrees C', 0.5556, -32)

-- from https://stackoverflow.com/a/6861858

代码复用

// 其实也可以不使用这些花里胡哨的柯里化啊,
// 偏函数应用啊什么的东东?
function converter (
  ratio, 
  symbol, 
  input
) {
  return (input * ratio).toFixed(2) + 
    ' ' + symbol
}

converter(2.2, 'lbs', 4)
converter(1.62, 'km', 34)
converter(1.98, 'US pints', 2.4)
converter(1.75, 'imperial pints', 2.4)

-- from https://stackoverflow.com/a/32379766

假如函数所需的参数无法同时得到?

3.3.函数组合

Function Composition

  • 3.3.1.组合的概念

如果 y 是 w 的函数,w 又是 x 的函数,即 y = f(w), w = g(x),那么 y 关于 x 的函数 y = f[g(x)] 叫做函数 y = f(w) 和 w = g(x) 的复合函数。其中 w 是中间变量,x 是自变量,y 是函数值。

-- from 高中数学 复合函数

此外在离散数学里,应该还学过复合函数  f(g(h(x))) 可记为 (f ○ g ○ h)(x)。

 

-- 其实这就是函数组合

  • 3.3.2.组合的实现
const add1 = x => x + 1
const mul3 = x => x * 3
const div2 = x => x / 2

// 结果是 3,但这样写可读性太差了
div2(mul3(add1(add1(0)))) 

const operate = 
  compose(div2, mul3, add1, add1)

operate(0) // => 相当于 div2(mul3(add1(add1(0))))
operate(2) // => 相当于 div2(mul3(add1(add1(2))))
// redux 版
const compose = (...fns) => {
  if (fns.length === 0) return arg => arg
  if (fns.length === 1) return fns[0]

  return fns.reduce((a, b) => (...args) => a(b(...args)))
}

// 一行版,支持多参数,但必须至少传一个函数
const compose = (...fns) => fns.reduceRight((acc, fn) => (...args) => fn(acc(...args)))

// 一行版,只支持单参数,但支持不传函数
const compose = (...fns) => arg => fns.reduceRight((acc, fn) => fn(acc), arg)

3.3.函数组合

Function Composition

  • 3.3.3.Pointfree

3.3.函数组合

Function Composition

Pointfree 即不使用所要处理的值,只合成运算过程。中文可以译作"无值"风格。  -- from Pointfree 编程风格指南

const addOne = x => x + 1
const square = x => x * x

const addOneThenSquare = compose(square, addOne)
addOneThenSquare(2) //  9
// 非 Pointfree,因为提到了数据:word
const snakeCase = function (word) {
  return word.toLowerCase().replace(/\s+/ig, '_')
}

// Pointfree
const snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase)

然而可惜的是,以上很 Pointfree 的代码会报错

因为在 JSreplace 和 toLowerCase 函数是定义在 String 的原型链上的...

定义时,根本不需要提到要处理的值

  • 3.3.3.Pointfree

3.3.函数组合

Function Composition

此外有的库(如 Underscore、Lodash...)把需要处理的数据放到了第一个参数。

const square = n => n * n;

_.map([4, 8], square) // 第一个参数是待处理数据

R.map(square, [4, 8]) // 一般函数式库都将数据放在最后

1.无法柯里化后偏函数应用

2.无法进行函数组合

3.无法扩展 map(reduce 等方法) 到各种其他类型

 

-- 详情参阅参考文献之《Hey Underscore, You're Doing It Wrong!》

  • 3.3.4.函数组合的意义

3.3.函数组合

Function Composition

一个应用其实就是一个长时间运行的进程,并将一系列异步的事件转换为对应结果。

Start 可以是:

  • 开启应用
  • DOM 事件(DOMContentLoaded, onClick, onSubmit...)
  • 接收到的 HTTP 请求
  • 返回的 HTTP 响应
  • 查询数据库的结果
  • WebSocket 消息
  • ..

End 可以是:

  • 渲染或更新 UI
  • 触发一个 DOM 事件
  • 创建一个 HTTP 请求
  • 返回一个 HTTP 响应
  • 保存数据到 DB
  • 发送 WebSocket 消息
  • ...
  • 3.3.4.函数组合的意义

3.3.函数组合

Function Composition

StartEnd 之间的东东,我们可以看做数据流的变换(transformations)。

这些变换具体的说就是一系列的变换动词结合

这些动词描述了这些变换做了些什么(而不是怎么做)如:

  • filter
  • slice
  • map
  • reduce
  • concat
  • zip
  • fork
  • flatten
  • ...

然而日常的数据流...

  • 3.4.1.基本概念

3.4.Hindley-Milner 类型签名

Hindley-Milner Type Signatures

//  id :: a -> a
const id = x => x

//  map :: (a -> b) -> [a] -> [b]
const map = curry((f, xs) => xs.map(f))

// strLength :: String -> Number
const strLength = s => s.length

// join :: String -> [String] -> String
const join = 
  curry((what, xs) => xs.join(what))

// match :: Regex -> String -> [String]
const match = 
  curry((reg, s) => s.match(reg))

// replace :: Regex -> String -> String -> String
const replace = curry((reg, sub, s) => s.replace(reg, sub))

在 Hindley-Milner 系统中,函数都写成类似 a -> b 这个样子,其中 a 和 b 是任意类型的变量。

多参函数为啥没有括号?

// 柯里化后可以不用一次传所有参数来调函数,
// 相当于首先传入 Regex 然后返回一个函数
// match :: Regex -> (String -> [String])
const match = 
  curry((reg, s) => s.match(reg))

// 可以看出柯里化后每传一个参数,
// 就会弹出类型签名最前面的那个类型。
// 以下就是传入 Regex 后返回的新函数
// onHoliday :: String -> [String]
const onHoliday = match(/holiday/ig)
  • 3.4.1.基本概念

3.4.Hindley-Milner 类型签名

Hindley-Milner Type Signatures

//  id :: a -> a
const id = x => x

//  head :: [a] -> a
const head = xs => xs[0]

//  filter :: (a -> Bool) -> [a] -> [a]
const filter = curry((f, xs) => xs.filter(f))

//  reduce :: (b -> a -> b) -> b -> [a] -> b
const reduce = curry((f, x, xs) => xs.reduce(f, x))

a -> b 可以是从任意类型的 a 到任意类型的 b,但是 a -> a 必须是同一个类型

例如,id 函数可以是 String -> String,也可以是 Number -> Number,但不能是 String -> Bool。

函数用括号包起来

同一类型变量

  • 3.4.2.参数态(Parametricity)

3.4.Hindley-Milner 类型签名

Hindley-Milner Type Signatures

这个特性表明,函数将会以一种统一的行为作用于所有的类型。

// reverse :: [a] -> [a]

以 reverse 函数为例,a 任意类型,那么函数对每一个可能的类型的操作都必须保持统一。

仅从类型签名来看,reverse 可能的目的是什么?

  • 它可以排序么?我觉得不行,我觉得很普通~,没有足够的信息让它去为每一个可能的类型排序。
  • 它能重新排列么?我觉得还 ok,但它必须以一种可预料的方式达成目标。

Hoogle 是个针对 Haskell 的 api 搜索引擎,

厉害的地方是可以用类型签名来搜索...

  • 3.4.3.自由定理(Free Theorems)

3.4.Hindley-Milner 类型签名

Hindley-Milner Type Signatures

// head :: [a] -> a
compose(f, head) === compose(head, map(f))

// filter :: (a -> Bool) -> [a] -> [a]
// 其中 p 是谓词函数
compose(map(f), filter(compose(p, f))) === 
  compose(filter(p), map(f)) 

来自 Wadler 的论文

Theorems for free!

  • 第一个例子意思是先取数组的第一个元素再对其应用函数 f,这操作与对整个数组都应用 f 再取第一个元素是等价的,但是前者要快得多。
  • 第二个例子意思是先从数组中根据谓词函数 f、g 过滤出部分元素再对其应用函数 f,这操作与先对整个数组应用函数 f,再根据函数 g 过滤出相应元素是等价的,同样前者要快得多。

常识?但计算机没有常识...所以根据自由定理可以做一些计算优化~

  • 3.4.4.类型约束

3.4.Hindley-Milner 类型签名

Hindley-Milner Type Signatures

签名也可以把类型约束为一个特定的接口(interface)

// sort :: Ord a => [a] -> [a]

a 一定是个 Ord 对象,或者说 a 必须要实现 Ord 接口。

// assertEqual :: (Eq a, Show a) => a -> a -> Assertion

两个类型约束:用于比较是否相等,并在不相等时打印差异

  • 3.4.5.类型签名的作用
  • 声明函数的输入和输出
  • 让函数保持通用和抽象
  • 可以用于编译时候检查
  • 代码最好的文档

四、Talk is cheap!

Show me the ... MONEY!

4.1.容器

Box

const nextCharForNumStr = (str) =>
  String.fromCharCode(parseInt(str.trim()) + 1)

nextCharForNumStr(' 64 ') // "A"
  • 1.怎么掐版

一层

二层

三层

四层

短短一行里居然

写了四层逻辑...

4.1.容器

Box

const nextCharForNumStr = (str) => {
  const trimmed = str.trim()
  const number = parseInt(trimmed)
  const nextNumber = number + 1
  return String.fromCharCode(nextNumber)
}
nextCharForNumStr(' 64 ') // 'A'
  • 2.改进版

这代码很不 pointfree...

const nextCharForNumStr = (str) => [str]
  .map(s => s.trim())
  .map(s => parseInt(s))
  .map(i => i + 1)
  .map(i => String.fromCharCode(i))

nextCharForNumStr(' 64 ') // ['A']
  • 3.借助数组版

包在数组里

4个map写逻辑

4.1.容器

Box

const Box = (x) => ({
  map: f => Box(f(x)),        // 返回容器为了链式调用
  fold: f => f(x),            // 将元素从容器中取出
  inspect: () => `Box(${x})`, // 看容器里有啥
})

const nextCharForNumStr = (str) => Box(str)
  ...
  .fold(c => c.toLowerCase()) // 可以轻易地继续调用新的函数

nextCharForNumStr(' 64 ') // 'a'
  • Box 定义

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

Box(1) === Box.of(1)

4.1.容器

Box

  • 函子(Functor)

来自范畴学的概念

functor 是实现了 map 函数

并遵守一些特定规则的容器类型。

1. 规则一:

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

其实就是

函数组合

2. 规则二:

const id = x => x

fx.map(id) === id(fx)

4.2.Either

  • 需求

获取对应颜色的十六进制的 RGB 值,并返回去掉`#`后的大写值。

const findColor = (name) => ({
  red: '#ff4444',
  blue: '#3b5998',
  yellow: '#fff68f',
})[name]

const redColor = findColor('red')
  .slice(1)
  .toUpperCase() // FF4444

const greenColor = findColor('green')
  .slice(1)
  .toUpperCase() 
// Uncaught TypeError: 
// Cannot read property 'slice' of undefined

4.2.Either

  • Either 定义和实现
// Either 由 Right 和 Left 组成

const Left = (x) => ({
  map: f => Left(x),            // 忽略传入的 f 函数
  fold: (f, g) => f(x),         // 使用左边的函数
  inspect: () => `Left(${x})`,  // 看容器里有啥
})

const Right = (x) => ({
  map: f => Right(f(x)),        // 返回容器为了链式调用
  fold: (f, g) => g(x),         // 使用右边的函数
  inspect: () => `Right(${x})`, // 看容器里有啥
})

either 类似于 box,主要的不同是 fold 函数要传递两个函数。

并且对于 left 来说 map 函数会忽略传入的函数。

4.2.Either

  • 使用 either 的解法
const fromNullable = (x) => x == null
  ? Left(null)
  : Right(x)

const findColor = (name) => fromNullable(({
  red: '#ff4444',
  blue: '#3b5998',
  yellow: '#fff68f',
})[name])

findColor('green')
  .map(c => c.slice(1))
  .fold(
    e => 'no color',
    c => c.toUpperCase()
  ) // no color

Left(null)

Left(null)

'no color'

4.3.Chain/FlatMap/bind/>>=

  • 需求
// chain.js
const fs = require('fs')

const getPort = () => {
  try {
    const str = fs.readFileSync('config.json')
    const { port } = JSON.parse(str)
    return port
  } catch(e) {
    return 3000
  }
}

const result = getPort() // 8888
// config.json
{ "port": 8888 }

读取 json 中的 port,出错就返回 3000

4.3.Chain/FlatMap/bind/>>=

  • 使用 Either 重构一把 
const tryCatch = (f) => {
  try {
    return Right(f())
  } catch (e) {
    return Left(e)
  }
}

const getPort = () => tryCatch(
    () => fs.readFileSync('config.json')
  )
  .map(c => JSON.parse(c))
  .fold(e => 3000, c => c.port)

这段代码有问题么?

parse 出错了咋办?

用 tryCatch 再包一层何如?

这里再包一层就变成:

  • Right(Right(''))
  • Right(Left(e))

4.3.Chain/FlatMap/bind/>>=

  • 用 tryCatch + chain 再包一层
const Left = (x) => ({
  ...
  chain: f => Left(x) // 和 map 一样,直接返回 Left
})

const Right = (x) => ({
  ...
  chain: f => f(x),   // 直接返回,不使用容器再包一层了
})

const getPort = () => tryCatch(
    () => fs.readFileSync('config.json')
  )
  // 使用 chain 和 tryCatch
  .chain(c => tryCatch(() => JSON.parse(c))) 
  .fold(e => 3000, c => c.port)

4.3.Chain/FlatMap/bind/>>=

  • 单子(Monad)

monad 是实现了 chain 函数并遵守一些特定规则的容器类型。

// 这里的 m 指的是一种 Monad 实例
const join = m => m.chain(x => x)

1. 规则一:

join(m.map(join)) === join(join(m)

2. 规则二:

// 这里的 M 指的是一种 Monad 类型
join(M.of(m) === join(m.map(M.of))
m.map(f) === m.chain(x => M.of(f(x)))

说明了 map 可被 chain 和 of 所定义

monad 一定是 functor

4.4.半群

Semigroup

定义一:对于非空集合 S,若在 S 上定义了二元运算 ○,使得对于任意的 a, b ∈ S,有 a ○ b ∈ S,则称 {S, ○} 为广群

定义二:若 {S, ○} 为广群,且运算 ○ 还满足结合律,即:任意 a, b, c ∈ S,有 (a ○ b) ○ c = a ○ (b ○ c),则称 {S, ○} 为半群

// 字符串和 concat 是半群
'1'.concat('2').concat('3') === '1'.concat('2'.concat('3'))

// 数组和 concat 是半群
[1].concat([2]).concat([3]) === [1].concat([2].concat([3]))

例如 JavaScript 中有 concat 方法的对象都是半群。

  • 半群的定义

4.4.半群

Semigroup

const Sum = (x) => ({
  x,
  concat: ({ x: y }) => Sum(x + y), // 采用解构获取值
  inspect: () => `Sum(${x})`,
})

Sum(1).concat(Sum(2)).inspect() // Sum(3)

那么对于 <Number, +> 来说它符合半群的定义吗?

  • 数字相加返回的仍然是数字(广群)
  • 加法满足结合律(半群)

但是数字并没有 concat 方法...

  • Sum

4.4.半群

Semigroup

const All = (x) => ({
  x,
  concat: ({ x: y }) => All(x && y), // 采用解构获取值
  inspect: () => `All(${x})`,
})

All(true).concat(All(false)).inspect() // All(false)

同理, <Boolean, &&> 也满足半群的定义

  • All 与 First
const First = (x) => ({
  x,
  concat: () => First(x), // 忽略后续的值
  inspect: () => `First(${x})`,
})

First('blah').concat(First('yo')).inspect() // First('blah')

对于字符串创建一个新的半群 <String, First>(只返回第一个参数)

4.4.半群

Semigroup

const data1 = {
  name: 'steve',
  isPaid: true,
  points: 10,
  friends: ['jame'],
}

假设有以下两个数据,需要将其合并

  • 半群的应用

First

All

Sum

Array

const concatObj = (obj1, obj2) => Object.entries(obj1)
  .map(([ key, val ]) => ({
    // concat 两个对象的值
    [key]: val.concat(obj2[key]),
  }))
  .reduce((acc, cur) => ({ ...acc, ...cur }), {})

concatObj(data1, data2)
const data2 = {
  name: 'steve',
  isPaid: false,
  points: 2,
  friends: ['young'],
}

4.5.幺半群

Monoid

  • 幺半群的定义

幺半群是一个存在单位元(幺元)的半群。

单位元:对于半群 <S, ○>,存在 e ∈ S,

使得任意 a ∈ S 有 a ○ e = e ○ a

  • 对于 <Number, +> 来说单位元就是 0
  • 对于 <Number, *> 来说单位元就是 1
  • 对于 <Boolean, &&> 来说单位元就是 true
  • 对于 <Boolean, ||> 来说单位元就是 false
  • 对于 <Number, Min> 来说单位元就是 Infinity
  • 对于 <Number, Max> 来说单位元就是 -Infinity

那么 <String, First> 是幺半群么?

4.5.幺半群

Monoid

  • <String, First> 不是幺半群
// 我们找不到这样的单位元 e 满足以下等式
First(e).concat(First('str')) === 
  First('str').concat(First(e))
  • 安全的幺半群
const sum = xs => xs.reduce((acc, cur) => acc + cur, 0)
const all = xs => xs.reduce((acc, cur) => acc && cur, true)

// first 没有单位元
const first = xs => xs.reduce(acc, cur) => acc)

sum([])   // 0,而不是报错!
all([])   // true,而不是报错!
first([]) // boom!!!

4.6.foldMap

  • 1.套路
const Monoid = (x) => ({ ... })

const monoid = xs => xs.reduce(
  (acc, cur) => acc.concat(cur),  // 使用 concat 结合
  Monoid.empty()                  // 传入幺元
)

monoid([Monoid(a), Monoid(b), Monoid(c)]) // 传入幺半群实例
  • 2.List、Map —— from immutable

顾名思义,正好对应原生的 Array 和 Object

然而库中并没有定义 empty 属性和 fold 方法

4.6.foldMap

  • 3.利用 List、Map 重构
import { List, Map } from 'immutable'

const derived = {
  fold (empty) {
    return this.reduce(
      (acc, cur) => acc.concat(cur), 
      empty
    )
  },
}

List.prototype.empty = List()
List.prototype.fold = derived.fold
Map.prototype.empty = Map({})
Map.prototype.fold = derived.fold

// from https://github.com/DrBoolean/immutable-ext

4.6.foldMap

  • 3.利用 List、Map 重构
List.of(1, 2, 3)
  .map(Sum)
  .fold(Sum.empty())     // Sum(6)

List().fold(Sum.empty()) // Sum(0)

Map({ steve: 1, young: 3 })
  .map(Sum)
  .fold(Sum.empty())     // Sum(4)

Map().fold(Sum.empty())  // Sum(0)

注意到 map 和 fold 这两步操作,从逻辑上来说是一个操作,所以我们可以新增 foldMap 方法来结合两者。

4.6.foldMap

  • 4.利用 foldMap 重构
const derived = {
  fold (empty) { ... },
  foldMap (f, empty) {
    return empty != null
      // 幺半群中将 f 的调用放在 reduce 中,提高效率
      ? this.reduce(
          (acc, cur, idx) => acc.concat(f(cur, idx)),
          empty
      )
      : this.map(f) // 在 map 中调用 f 是因为考虑到空的情况
        .reduce((acc, cur) => acc.concat(cur))
  },
}

List.of(1, 2, 3).foldMap(Sum, Sum.empty())    // Sum(6)
List().foldMap(Sum, Sum.empty())              // Sum(0)
Map({ a: 1, b: 3 }).foldMap(Sum, Sum.empty()) // Sum(4)
Map().foldMap(Sum, Sum.empty())               // Sum(0)

4.7.拖延症容器

虽然你可以不停地用 map 给它分配任务,

但是只要你不调用 fold 方法催它执行,它就死活不执行...

const LazyBox = (g) => ({
  map: f => LazyBox(() => f(g())),
  fold: f => f(g()),
})

const result = LazyBox(() => ' 64 ')
  .map(s => s.trim())
  .map(i => parseInt(i))
  .map(i => i + 1)
  .map(i => String.fromCharCode(i))
  // 没有 fold 死活不执行

result.fold(c => c.toLowerCase()) // a

LazyBox

4.8.任务

Task

  • 1.基本介绍
import Task from 'data.task'

const showErr = e => console.log(`err: ${e}`)
const showSuc = x => console.log(`suc: ${x}`)

Task.of(1).fork(showErr, showSuc) // suc: 1

Task.of(1).map(x => x + 1)
  .fork(showErr, showSuc) // suc: 2

// 类似 Left
Task.rejected(1).map(x => x + 1)
  .fork(showErr, showSuc) // err: 1

Task.of(1).chain(x => new Task.of(x + 1))
  .fork(showErr, showSuc) // suc: 2

4.8.任务

Task

  • 2.使用示例
const lauchMissiles = () => (
  // 和 promise 很像,不过 promise 会立即执行
  // 而且参数的位置也相反
  new Task((rej, res) => {
    console.log('lauchMissiles')
    res('missile')
  })
)

// 继续对之前的任务添加后续操作(duang~给飞弹加特技!)
const app = lauchMissiles()
  .map(x => x + '!')

// 这时才执行(发射飞弹)
app.fork(showErr, showSuc)

4.8.任务

Task

  • 3.原理意义

我们将有副作用的代码给包起来之后,

这些新函数就都变成了纯函数,

这样我们的整个应用的代码都是纯的~,

并且在代码真正执行前(fork 前)

还可以不断地 compose 别的函数增加逻辑。

4.8.任务

Task

  • 4.异步嵌套示例
  1. 读取 config1.json 中的数据
  2. 将内容中的 8 替换成 6
  3. 将新内容写到 config2.json 中
import fs from 'fs'

const app = () => (
  fs.readFile('config1.json', 'utf-8', (err, contents) => {
    if (err) throw err

    const newContents = content.replace(/8/g, '6')

    fs.writeFile('config2.json', newContents, (err, _) => {
      if (err) throw err

      console.log('success!')
    })
  })
)

4.8.任务

Task

  • 4.异步嵌套示例
  1. 读取 config1.json 中的数据
  2. 将内容中的 8 替换成 6
  3. 将新内容写到 config2.json 中
import fs from 'fs'
import Task from 'data.task'

const cfg1 = 'config1.json'
const cfg2 = 'config2.json'

const readFile = (file, enc) => (
  new Task((rej, res) =>
    fs.readFile(file, enc, (err, str) =>
      err ? rej(err) : res(str)
    )
  )
)

const writeFile = (file, str) => (
  new Task((rej, res) =>
    fs.writeFile(file, str, (err, suc) =>
      err ? rej(err) : res(suc)
    )
  )
)
const app = readFile(cfg1, 'utf-8')
  .map(
    str => str.replace(/8/g, '6')
  )
  .chain(
    str => writeFile(cfg2, str)
  )

app.fork(
  e => console.log(`err: ${e}`),
  x => console.log(`suc: ${x}`)
)

4.9.Applicative Functor

  • 1.问题引入

Applicative Functor 提供了让

不同的函子(functor)互相应用的能力。

const add = x => y => x + y

add(Box.of(2))(Box.of(3)) // NaN

Box(2).map(add).inspect() // Box(y => 2 + y)

为啥我们需要函子的互相应用?什么是互相应用?

现在我们有了一个容器,它的内部值为

局部调用(partially applied)后的函数。

接着想让它应用到 Box(3) 上,最后得到 Box(5) 的预期结果。

4.9.Applicative Functor

  • 1.问题引入 —— chain
Box(2)
  .chain(x => Box(3).map(add(x)))
  .inspect() // Box(5)

成功实现~,BUT,这种实现方法有个问题,

那就是单子(Monad)的执行顺序问题。

首先执行

等待 Box(2) 执行完毕

必须等 Box(2) 执行完毕后,

才能对 Box(3) 进行求值。

假如这是两个异步任务,

那么完全无法同时并行执行。

4.9.Applicative Functor

  • 2.基本介绍
const Box = (x) => ({
  // 这里 box 是另一个 Box 的实例,x 是函数
  ap: box => box.map(x), 
  ...
})

Box(add)
  // Box(y => 2 + y) ,咦?在哪儿见过?
  .ap(Box(2)) 
  .ap(Box(3)) // Box(5)
F(x).map(f) === F(f).ap(F(x))

// 这就是为什么
Box(2).map(add) === Box(add).ap(Box(2))

运算规则

4.9.Applicative Functor

  • 3.Lift 家族
// F 该从哪儿来?
const fakeLiftA2 = f => fx => fy => F(f).ap(fx).ap(fy)

// 应用运算规则转换一下~
const liftA2 = f => fx => fy => fx.map(f).ap(fy)

liftA2(add, Box(2), Box(4)) // Box(6)

// 同理
const liftA3 = f => fx => fy => fz => 
  fx.map(f).ap(fy).ap(fz)

const liftA4 = ...
...
const liftAN = ...

4.9.Applicative Functor

  • 4.Lift 应用
// 假装是个 jQuery 接口~
const $ = selector =>
  Either.of({ selector, height: 10 })

const getScreenSize = screen => head => foot =>
  screen - (head.height + foot.height)

// Right(780)
liftA2(getScreenSize(800))($('header'))($('footer'))
// List 的笛卡尔乘积
List.of(x => y => z => [x, y, z].join('-'))
  .ap(List.of('tshirt', 'sweater'))
  .ap(List.of('white', 'black'))
  .ap(List.of('small', 'medium', 'large'))

4.9.Applicative Functor

  • 4.Lift 应用
const Db = ({
  find: (id, cb) =>
    new Task((rej, res) =>
      setTimeout(() => res({ id, title: `${id}`}), 100)
    )
})

const reportHeader = (p1, p2) =>
  `Report: ${p1.title} compared to ${p2.title}`

Task.of(p1 => p2 => reportHeader(p1, p2))
  .ap(Db.find(20))
  .ap(Db.find(8))
  // Report: 20 compared to 8
  .fork(console.error, console.log)

liftA2
  (p1 => p2 => reportHeader(p1, p2))
  (Db.find(20))
  (Db.find(8))
  // Report: 20 compared to 8
  .fork(console.error, console.log)

4.10.Traversable

import fs from 'fs'

// 详见 4.8.
const readFile = (file, enc) => (
  new Task((rej, res) => ...)
)

const files = ['a.js', 'b.js']

// [Task, Task],我们得到了一个 Task 的数组
files.map(file => readFile(file, 'utf-8'))

然而我们想得到的是一个包含数组的 Task([file1, file2])

这样就可以调用它的 fork 方法,查看执行结果。

  • 1.问题引入
files
  .traverse(Task.of, file => readFile(file, 'utf-8'))
  .fork(console.error, console.log)

4.10.Traversable

Array.prototype.empty = []

// traversable
Array.prototype.traverse = function (point, fn) {
  return this.reduce(
    (acc, cur) => acc
      .map(z => y => z.concat(y))
      .ap(fn(cur)),
    point(this.empty)
  )
}
  • 2.实现

单位元

acc: 累加器

看到代码先别晕~,让咱们来分析下:

  1. reduce: 从左到右遍历元素
  2. z => y => z.concat(y): 函子间的调用函数,合并半群
  3. fn(cur): 被调用的函子

所以相当于遍历一遍数组,将每个元素用 fn 调用合并返回

cur: 当前值

合并半群

4.11.自然变换

Natural Transformation

  • 1.基本概念

自然变换就是一个函数,接受一个函子(functor),返回另一个函子

const boxToEither = b => b.fold(Right)

Box

Either

自然变换

Left?

nt(x).map(f) == nt(x.map(f))

满足规则:

const res1 = boxToEither(Box(100))
  .map(x => x * 2)
const res2 = boxToEither(
  Box(100).map(x => x * 2)
)

res1 === res2 // Right(200)

4.11.自然变换

Natural Transformation

  • 2.应用场景
const arr = [2, 400, 5, 1000]
const first = xs => fromNullable(xs[0])
const double = x => x * 2
const getLargeNums = xs => xs.filter(x => x > 100)

first(
  getLargeNums(arr)
    .map(double)
)

// 根据自然变换,它显然和下面的式子是等价的
// 但是下式的性能显然好得多
first(getLargeNums(arr))
  .map(double)

例1:得到一个数组小于等于 100 的最后一个数的两倍的值

4.11.自然变换

Natural Transformation

  • 2.应用场景

例2:找到 id 为 3 的用户的最好的朋友的 id

// Task(Either(user))
const zero = Db.find(3)

// 第一版
// Task(Either(Task(Either(user)))) ???
const one = zero
  .map(either => either
    .map(user => Db
      .find(user.bestFriendId)
    )
  )
  .fork(
    console.error,
    either => either // Either(Task(Either(user)))
      .map(t => t.fork( // Task(Either(user))
        console.error,
        either => either
            .map(console.log), // Either(user)
      ))
  )
// 假 api
const fakeApi = (id) => ({
  id,
  name: 'user1',
  bestFriendId: id + 1,
})

// 假 Db
const Db = {
  find: (id) => new Task(
    (rej, res) => (
      res(id > 2
        ? Right(fakeApi(id))
        : Left('not found')
      )
    )
  )
}

4.11.自然变换

Natural Transformation

  • 2.应用场景

例2:找到 id 为 3 的用户的最好的朋友的 id

// Task(Either(user))
const zero = Db.find(3)

// 第二版
const two = zero
  .chain(either => either
    .fold(Task.rejected, Task.of) // Task(user)
    .chain(user => Db
      .find(user.bestFriendId) // Task(Either(user))
    )
    .chain(either => either
      .fold(Task.rejected, Task.of) // Task(user)
    )
  )
  .fork(
    console.error,
    console.log,
  )

多余嵌套

// 假 api
const fakeApi = (id) => ({
  id,
  name: 'user1',
  bestFriendId: id + 1,
})

// 假 Db
const Db = {
  find: (id) => new Task(
    (rej, res) => (
      res(id > 2
        ? Right(fakeApi(id))
        : Left('not found')
      )
    )
  )
}

4.11.自然变换

Natural Transformation

  • 2.应用场景
// 假 api
const fakeApi = (id) => ({
  id,
  name: 'user1',
  bestFriendId: id + 1,
})

// 假 Db
const Db = {
  find: (id) => new Task(
    (rej, res) => (
      res(id > 2
        ? Right(fakeApi(id))
        : Left('not found')
      )
    )
  )
}

例2:找到 id 为 3 的用户的最好的朋友的 id

// Task(Either(user))
const zero = Db.find(3)

// 第三版
const three = zero 
  .chain(either => either
    .fold(Task.rejected, Task.of) // Task(user)
  )
  .chain(user => Db
    .find(user.bestFriendId) // Task(Either(user))
  )
  .chain(either => either
    .fold(Task.rejected, Task.of) // Task(user)
  )
  .fork(
    console.error,
    console.log,
  )

重复逻辑

4.11.自然变换

Natural Transformation

  • 2.应用场景

例2:找到 id 为 3 的用户的最好的朋友的 id

// 将 Either 变换成 Task
const eitherToTask = (e) => (
  e.fold(Task.rejected, Task.of)
)

// 第四版
const four = zero
  .chain(eitherToTask) // Task(user)
  .chain(user => Db
    // Task(Either(user))
    .find(user.bestFriendId)
  )
  .chain(eitherToTask) // Task(user)
  .fork(
    console.error,
    console.log,
  )

自然变换

// 出错版
const error = Db.find(2)
  // Task.rejected('not found')
  .chain(eitherToTask) 
  // 这里永远不会被调用,被跳过了
  .chain(() => console.log('hey man')) 
  ...
  .fork(
    console.error, // not found
    console.log,
  )

4.12.同构

Isomorphism

  • 1.定义

同构是在数学对象之间定义的一类映射,它能揭示出在这些对象的属性或者操作之间存在的关系。

简单来说就是两种不同类型的对象经过变形,保持结构并且不丢失数据。

to(from(x)) === x

from(to(y)) === y

一对儿函数

from/to

无损地保存同样的信息

4.12.同构

Isomorphism

  • 2.应用场景

例1:[Char] 和 String 同构

// String ~ [Char]
const Iso = (to, from) => ({ to, from })

const chars = Iso(
  s => s.split(''),
  c => c.join('')
)

const str = 'hello world'

chars.from(chars.to(str)) === str

这能有啥用呢?

const truncate = (str) => (
  chars.from(
    // 我们先用 to 方法将其转成数组
    // 这样就能使用数组的各类方法
    chars.to(str).slice(0, 3)
  ).concat('...')
)

truncate(str) // hel...

4.12.同构

  • 2.应用场景

例2:再来看看最多有一个参数的数组 [a] 和 Either 的同构关系

// [a] ~ Either null a
const singleton = Iso(
  e => e.fold(() => [], x => [x]),
  ([ x ]) => x ? Right(x) : Left()
)

const filterEither = (e, pred) => singleton
    .from(
      singleton
        .to(e)
        .filter(pred)
    )
const getUCH = str => filterEither(
  Right(str),
  x => x.match(/h/ig)
).map(x => x.toUpperCase())

getUCH('hello') // Right(HELLO)

getUCH('ello') // Left(undefined)

Isomorphism

λ演算 VS 图灵机

Alonzo Church VS Alan Turing

参考文献

functional-programming-in-javascript

By steve young

functional-programming-in-javascript

JavaScript函数式编程

  • 7,552