函数式编程术语及示例

PatWatkins 2年前
   <p>函数式编程有许多优势,由此越来越受欢迎。然而每个编程范式 (paradigm) 都有自己唯一的术语,函数式编程也不例外。我们提供一张术语表,希望使你学习函数式编程变得容易些。</p>    <p>示例均为 javascript (ES2015)。 Why javascript</p>    <p>尚在 WIP 阶段,欢迎 pr。</p>    <p>如有可能,本篇文档术语由 Fantasy Land spec 定义。</p>    <p>目录</p>    <ul>     <li>偏函数应用 (Partial Application)</li>     <li>自动柯里化 (Auto Currying)</li>     <li>函数组合 (Function Composition)</li>     <li>副作用 (Side effects)</li>     <li>Point-Free 风格 (Point-Free Style)</li>     <li>      <ul>       <li>一致性 (Preserves identity)</li>      </ul> </li>     <li>引用透明性 (Referential Transparency)</li>     <li>惰性求值 (Lazy evaluation)</li>     <li>      <ul>       <li>自同态 (Endomorphism)</li>      </ul> </li>     <li>类型签名 (Type Signatures)</li>    </ul>    <h2>Arity</h2>    <p>函数参数的个数。来自于单词 unary, binary, ternary 等等。这个单词是由 -ary 与 -ity 两个后缀拼接而成。例如,一个带有两个参数的函数被称为二元函数或者它的 arity 是2。它也被那些更喜欢希腊词根而非拉丁词根的人称为 dyadic 。同样地,带有可变数量参数的函数被称为 variadic ,而二元函数只能带两个参数。</p>    <pre>  <code class="language-javascript">const sum = (a, b) => a + b    const arity = sum.length  console.log(arity)        // 2</code></pre>    <h2>高阶函数 (Higher-Order Function / HOF)</h2>    <p>以函数为参数或/和返回值。</p>    <pre>  <code class="language-javascript">const filter = (predicate, xs) => xs.filter(predicate)    const is = (type) => (x) => Object(x) instanceof type    filter(is(Number), [0, '1', 2, null]) // 0, 2</code></pre>    <h2>偏函数 (Partial Function)</h2>    <p>对原始函数预设参数作为一个新的函数。</p>    <pre>  <code class="language-javascript">// 创建偏函数,固定一些参数  const partical = (f, ...args) =>    // 返回一个带有剩余参数的函数    (...moreArgs) =>      // 调用原始函数      f(...args, ...moreArgs)    const add3 = (a, b, c) => a + b + c    // (...args) => add3(2, 3, ...args)  // (c) => 2 + 3 + c  const fivePlus = partical(add3, 2, 3)    fivePlus(4)  // 9</code></pre>    <p>也可以使用 Function.prototype.bind 实现偏函数。</p>    <pre>  <code class="language-javascript">const add1More = add3.bind(null, 2, 3)</code></pre>    <p>偏函数应用通过对复杂的函数填充一部分数据来构成一个简单的函数。柯里化通过偏函数实现。</p>    <h2>柯里化 (Currying)</h2>    <p>将一个多元函数转变为一元函数的过程。 每当函数被调用时,它仅仅接收一个参数并且返回带有一个参数的函数,直到传递完所有的参数。</p>    <pre>  <code class="language-javascript">const sum = (a, b) => a + b    const curriedSum = (a) => (b) => a + b    curriedSum(3)(4)         // 7    const add2 = curriedSum(2)    add2(10)     // 12</code></pre>    <h2>自动柯里化 (Auto Currying)</h2>    <p>lodash , understore 和 ramda 有 curry 函数可以自动完成柯里化。</p>    <pre>  <code class="language-javascript">const add = (x, y) => x + y    const curriedAdd = _.curry(add)    curriedAdd(1, 2)   // 3  curriedAdd(1)(2)   // 3  curriedAdd(1)      // (y) => 1 + y</code></pre>    <p>进一步阅读</p>    <ul>     <li><a href="/misc/goto?guid=4959740942411729019" rel="nofollow,noindex">Favoring Curry</a></li>     <li><a href="/misc/goto?guid=4959667623610427024" rel="nofollow,noindex">Hey Underscore, You're Doing It Wrong!</a></li>    </ul>    <h2>函数组合 (Function Composing)</h2>    <p>接收多个函数作为参数,从右到左,一个函数的输入为另一个函数的输出。</p>    <pre>  <code class="language-javascript">const compose = (f, g) => (a) => f(g(a))    // 定义  const floorAndToString = compose((val) => val.toString(), Math.floor) // 使用  floorAndToString(12.12)   // '12'</code></pre>    <h2>Continuation</h2>    <p>在一个程序执行的任意时刻,尚未执行的代码称为 Continuation。</p>    <pre>  <code class="language-javascript">const printAsString = (num) => console.log(`Given ${num}`)    const addOneAndContinue = (num, cc) => {    const result = num + 1    cc(result)  }    addOneAndContinue(2, printAsString) // 'Given 3'</code></pre>    <p>Continuation 在异步编程中很常见,比如当程序需要接收到数据才能够继续执行。请求的响应通常作为代码的剩余执行部分,一旦接收到数据,对数据的处理被作为 Continuation。</p>    <pre>  <code class="language-javascript">const continueProgramWith = (data) => {    // 继续执行程序  }    readFileAsync('path/to/file', (err, response) => {    if (err) {      // 错误处理      return    }    continueProgramWith(response)  })</code></pre>    <h2>纯函数 (Purity)</h2>    <p>输出仅由输入决定,且不产生副作用。</p>    <pre>  <code class="language-javascript">const greet = (name) => `hello, ${name}`    greet('world')</code></pre>    <p>以下代码不是纯函数:</p>    <pre>  <code class="language-javascript">window.name = 'Brianne'    const greet = () => `Hi, ${window.name}`    greet() // "Hi, Brianne"</code></pre>    <p>以上示例中,函数依赖外部状态。</p>    <pre>  <code class="language-javascript">let greeting    const greet = (name) => {      greeting = `Hi, ${name}`  }    greet('Brianne')  greeting // "Hi, Brianne"</code></pre>    <p>以上实例中,函数修改了外部状态。</p>    <h2>副作用 (Side effects)</h2>    <p>如果函数与外部可变状态进行交互,则它是有副作用的。</p>    <pre>  <code class="language-javascript">const differentEveryTime = new Date()</code></pre>    <pre>  <code class="language-javascript">console.log('IO is a side effect!')</code></pre>    <h2>幂等性 (Idempotent)</h2>    <p>如果一个函数执行多次皆返回相同的结果,则它是幂等性的。</p>    <pre>  <code class="language-javascript">f(f(x)) ≍ f(x)</code></pre>    <pre>  <code class="language-javascript">Math.abs(Math.abs(10))</code></pre>    <pre>  <code class="language-javascript">sort(sort(sort([2, 1])))</code></pre>    <h2>Point-Free 风格 (Point-Free Style)</h2>    <p>定义函数时,不显式地指出函数所带参数。这种风格通常需要柯里化或者高阶函数。也叫 Tacit programming。</p>    <pre>  <code class="language-javascript">const map = (fn) => (list) => list.map(fn)  const add = (a) => (b) => a + b    # Points-Free   list 是显式参数  const incrementAll = (numbers) => map(add(1))(numbers)    # Points-Free   list 是隐式参数  const incrementAll2 = map(add(1))</code></pre>    <p>incrementAll 识别并且使用了 numbers 参数,因此它不是 Point-Free 风格的。 incrementAll2 连接函数与值,并不提及它所使用的参数,因为它是 Point-Free 风格的。</p>    <p>Point-Free 风格的函数就像平常的赋值,不使用 function 或者 => 。</p>    <h2>谓词 (Predicate)</h2>    <p>根据输入返回 true 或 false。通常用在 Array.prototype.filter 的回调函数中。</p>    <pre>  <code class="language-javascript">const predicate = (a) => a > 2    ;[1, 2, 3, 4].filter(predicate)</code></pre>    <h2>契约 (Contracts)</h2>    <p>契约保证了函数或者表达式在运行时的行为。当违反契约时,将抛出一个错误。</p>    <pre>  <code class="language-javascript">const contract = (input) => {    if (typeof input === 'number') return true    throw new Error('Contract Violated: expected int -> int')  }    const addOne = (num) => contract(num) && num + 1    addOne(2)  addOne('hello') // Error</code></pre>    <h2>Guarded Functions</h2>    <p>TODO</p>    <h2>范畴 (Category)</h2>    <p>在范畴论中,范畴是指对象集合及它们之间的态射 (morphism)。在编程中,数据类型作为对象,函数作为态射。</p>    <p>一个有效的范畴遵从以下三个原则:</p>    <ol>     <li>必有一个 identity 态射,使得 map 一个对象是它自身。 a 是范畴里的一个对象时,必有一个函数使 a -> a 。</li>     <li>态射必是可组合的。 a , b , c 是范畴里的对象, f 是态射 a -> b , g 是 b -> c 态射。 g(f(x)) 一定与 (g ● f)(x)</li>     <li>组合满足结合律。 f ● (g ● h) 与 (f ● g) ● h 是等价的。</li>    </ol>    <p>这些准则是非常抽象的,范畴论对与发现组合的新方法是伟大的。</p>    <p>进一步阅读</p>    <ul>     <li><a href="/misc/goto?guid=4959740942517083293" rel="nofollow,noindex">Category Theory for Programmers</a></li>    </ul>    <h2>值 (Value)</h2>    <p>赋值给变量的值称作 Value。</p>    <pre>  <code class="language-javascript">5  Object.freeze({name: 'John', age: 30})  ;(a) => a  ;[1]  undefined</code></pre>    <h2>常量 (Constant)</h2>    <p>一旦定义不可重新赋值。</p>    <pre>  <code class="language-javascript">const five = 5  const john = Object.freeze({name: 'John', age: 30})</code></pre>    <p>常量是的,因此它们可以被它们所代表的值替代而不影响结果。</p>    <p>对于以上两个常量,以下语句总会返回 true。</p>    <pre>  <code class="language-javascript">john.age + five === ({name: 'John', age: 30}).age + (5)</code></pre>    <h2>函子 (Functor)</h2>    <p>一个实现了map 函数的对象,map 会遍历对象中的每个值并生成一个新的对象。遵守两个准则</p>    <h3>一致性 (Preserves identity)</h3>    <pre>  <code class="language-javascript">object.map(x => x) ≍ object</code></pre>    <h3>组合性 (Composable)</h3>    <pre>  <code class="language-javascript">object.map(compose(f, g)) ≍ object.map(g).map(f)  // f, g 为任意函数</code></pre>    <p>在 javascript 中一个常见的函子是 Array, 因为它遵守因子的两个准则。</p>    <pre>  <code class="language-javascript">const f = x => x + 1  const g = x => x * 2    ;[1, 2, 3].map(x => f(g(x)))  ;[1, 2, 3].map(g).map(f)</code></pre>    <h3>Pointed Functor</h3>    <p>一个实现了 of 函数的对象。</p>    <p>ES2015 添加了 Array.of ,使 Array 成为了 Pointed Functor。</p>    <pre>  <code class="language-javascript">Array.of(1)</code></pre>    <h2>Lift</h2>    <p>TODO</p>    <h2>引用透明性 (Referential Transparency)</h2>    <p>一个表达式能够被它的值替代而不改变程序的行为成为引用透明。</p>    <pre>  <code class="language-javascript">const greet = () => 'hello, world.'</code></pre>    <h2>Equational Reasoning</h2>    <p>TODO</p>    <h2>匿名函数 (Lambda)</h2>    <p>匿名函数被视作一个值</p>    <pre>  <code class="language-javascript">;(function (a) {      return a + 1  })    ;(a) => a + 1</code></pre>    <p>匿名函数通常作为高阶函数的参数</p>    <pre>  <code class="language-javascript">[1, 2].map((a) => a + 1)</code></pre>    <p>可以把 Lambda 赋值给一个变量</p>    <pre>  <code class="language-javascript">const add1 = (a) => a + 1</code></pre>    <h2>Lambda Caculus</h2>    <p>数学的一个分支,使用函数创造 通过计算模型</p>    <h2>惰性求值 (Lazy evaluation)</h2>    <p>按需求值机制,只有当需要计算所得值时才会计算。</p>    <pre>  <code class="language-javascript">const rand = function* () {    while (true) {      yield Math.random()      }   }    const randIter = rand()  randIter.next()</code></pre>    <h2>Monoid</h2>    <p>一个对象拥有一个函数用来连接相同类型的对象。</p>    <p>数值加法是一个简单的 Monoid</p>    <pre>  <code class="language-javascript">1 + 1   // 2</code></pre>    <p>以上示例中,数值是对象而 + 是函数。</p>    <p>与另一个值结合而不会改变它的值必须存在,称为 identity 。</p>    <p>加法的 identity 值为 0:</p>    <pre>  <code class="language-javascript">1 + 0   // 1</code></pre>    <p>需要满足结合律</p>    <pre>  <code class="language-javascript">1 + (2 + 3) === (1 + 2) + 3 // true</code></pre>    <p>数组的结合也是 Monoid</p>    <pre>  <code class="language-javascript">;[1, 2].concat([3, 4])</code></pre>    <p>identity 值为空数组</p>    <pre>  <code class="language-javascript">;[1, 2].concat([])</code></pre>    <p>identity 与 compose 函数能够组成 monoid</p>    <pre>  <code class="language-javascript">const identity = (a) => a  const compose = (f, g) => (x) => f(g(x))</code></pre>    <p>foo 是只带一个参数的任意函数</p>    <pre>  <code class="language-javascript">compose(foo, identity) ≍ compose(identity, foo) ≍ foo</code></pre>    <h2>Monad</h2>    <p>拥有 of 和 chain 函数的对象。 chain 很像 map , 除了用来铺平嵌套数据。</p>    <pre>  <code class="language-javascript">Array.prototype.chain = function (f) {    return this.reduce((acc, it) => acc.concat(f(it)), [])    }    // ['cat', 'dog', 'fish', 'bird']  ;Array.of('cat,dog', 'fish,bird').chain(s => s.split(','))    // [['cat', 'dog'], ['fish', 'bird']]  ;Array.of('cat,dog', 'fish,bird').map(s => s.split(','))</code></pre>    <p>在有些语言中, of 也称为 return , chain 也称为 flatmap 与 bind 。</p>    <h2>Comonad</h2>    <p>拥有 extract 与 extend 函数的对象。</p>    <pre>  <code class="language-javascript">const CoIdentity = (v) => ({    val: v,    extract () {      return this.val      },    extend (f) {      return CoIdentity(f(this))      }  })</code></pre>    <pre>  <code class="language-javascript">CoIdentity(1).extract()  CoIdentity(1).extend(x => x.extract() + 1)   # CoIdentity(2)</code></pre>    <h2>Applicative Functor</h2>    <p>一个拥有 ap 函数的对象。</p>    <pre>  <code class="language-javascript">// 实现  Array.prototype.ap = function (xs) {      return this.reduce((acc, f) => acc.concat(xs.map(f)), [])  }    // 示例  ;[(a) => a + 1].ap([1]) // [2]</code></pre>    <p>如果你有两个对象,并需要对他们的元素执行一个二元函数</p>    <pre>  <code class="language-javascript">// Arrays that you want to combine  const arg1 = [1, 3]  const arg2 = [4, 5]    // combining function - must be curried for this to work  const add = (x) => (y) => x + y    const partiallyAppliedAdds = [add].ap(arg1) // [(y) => 1 + y, (y) => 3 + y]</code></pre>    <p>由此得到了一个函数数组,并且可以调用 ap 函数得到结果</p>    <pre>  <code class="language-javascript">partiallyAppliedAdds.ap(arg2) // [5, 6, 7, 8]</code></pre>    <h2>态射 (Morphism)</h2>    <p>一个变形的函数。</p>    <h3>自同态 (Endomorphism)</h3>    <p>输入输出是相同类型的函数。</p>    <pre>  <code class="language-javascript">// uppercase :: String -> String  const uppercase = (str) => str.toUpperCase()    // decrement :: Number -> Number  const decrement = (x) => x - 1</code></pre>    <h3>同构 (Isomorphism)</h3>    <p>不用类型对象的变形,保持结构并且不丢失数据。</p>    <p>例如,一个二维坐标既可以表示为数组 [2, 3] ,也可以表示为对象 {x: 2, y: 3} 。</p>    <pre>  <code class="language-javascript">// 提供函数在两种类型间互相转换  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}</code></pre>    <h2>Setoid</h2>    <p>拥有 equals 函数的对象。 equals 可以用来和其它对象比较。</p>    <pre>  <code class="language-javascript">Array.prototype.equals = function (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([3, 4])   // false</code></pre>    <h2>半群 (Semigroup)</h2>    <p>一个拥有 concat 函数的对象。 concat 可以连接相同类型的两个对象。</p>    <pre>  <code class="language-javascript">;[1].concat([2]) // [1, 2]</code></pre>    <h2>Foldable</h2>    <p>一个拥有 reduce 函数的对象。 reduce 可以把一种类型的对象转化为另一种类型。</p>    <pre>  <code class="language-javascript">const sum = (list) => list.reduce((acc, val) => acc + val, 0)  sum([1, 2, 3])        // 6</code></pre>    <h2>Traversable</h2>    <p>TODO</p>    <h2>类型签名 (Type Signatures)</h2>    <p>通常 js 会在注释中指出参数与返回值的类型。</p>    <pre>  <code class="language-javascript">// functionName :: firstArgType -> secondArgType -> returnType    // add :: Number -> Number -> Number  const add = (x) => (y) => x + y    // increment :: Number -> Number  const increment = (x) => x + 1</code></pre>    <p>如果函数的参数也是函数,那么这个函数需要用括号括起来。</p>    <pre>  <code class="language-javascript">// call :: (a -> b) -> a -> b  const call = (f) => (x) => f(x)</code></pre>    <p>字符 a, b, c, d 表明参数可以是任意类型。以下版本的 map 的参数 f,把一种类型 a 的数组转化为另一种类型 b 的数组。</p>    <pre>  <code class="language-javascript">// map :: (a -> b) -> [a] -> [b]  const map = (f) => (list) => list.map(f)</code></pre>    <h2>联合类型 (Union Type)</h2>    <p>连接不同的数据类型。</p>    <p>js 没有静态类型,我们假设一个数据类型是 NumOrString 用来对 Number 与 String 两种类型求和。</p>    <p>js 中可以对数值或字符串使用 + 操作符,因此我们可以使用这个新类型去描述输入输出。</p>    <pre>  <code class="language-javascript">// add :: (NumOrString, NumOrString) -> NumOrString  const add = (a, b) => a + b    add(1, 2) // Returns number 3  add('Foo', 2) // Returns string "Foo2"  add('Foo', 'Bar') // Returns string "FooBar"</code></pre>    <p>联合类型又称为代数类型 algebraic types,tagged union 或者 sum type。</p>    <p>这里有一些 js 库可以帮助我们定义和使用联合类型。</p>    <ul>     <li><a href="/misc/goto?guid=4959740942599464745" rel="nofollow,noindex">union-type</a></li>     <li><a href="/misc/goto?guid=4959740942674565147" rel="nofollow,noindex">daggy</a></li>    </ul>    <h2>Product type</h2>    <p>用一种你可能更熟悉的方式把数据类型联合起来</p>    <pre>  <code class="language-javascript">// point :: (Number, Number) -> {x: Number, y: Number}  const point = (x, y) => ({x: x, y: y})</code></pre>    <h2>Option</h2>    <p>Option 是一种联合类型,它有两种情况, Some 或者 None 。</p>    <pre>  <code class="language-javascript">// 定义  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])</code></pre>    <p>使用 chain 可以序列化返回 Option 的函数。</p>    <pre>  <code class="language-javascript">// 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)</code></pre>    <p>在其它的一些地方, Option 也称为 Maybe , Some 也称为 Just , None 也称为 Nothing 。</p>    <p> </p>    <p>来自:https://juejin.im/entry/58c02176128fe1006b1c5792</p>    <p> </p>