Loading

functional-programming-in-javascript

steve young

This is a live streamed presentation. You will automatically follow the presenter and see the slide they're currently on.

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

参考文献

Made with Slides.com