函數式編程術語及示例
函數式編程有許多優勢,由此越來越受歡迎。然而每個編程范式 (paradigm) 都有自己唯一的術語,函數式編程也不例外。我們提供一張術語表,希望使你學習函數式編程變得容易些。
示例均為 javascript (ES2015)。 Why javascript
尚在 WIP 階段,歡迎 pr。
如有可能,本篇文檔術語由 Fantasy Land spec 定義。
目錄
- 偏函數應用 (Partial Application)
- 自動柯里化 (Auto Currying)
- 函數組合 (Function Composition)
- 副作用 (Side effects)
- Point-Free 風格 (Point-Free Style)
-
- 一致性 (Preserves identity)
- 引用透明性 (Referential Transparency)
- 惰性求值 (Lazy evaluation)
-
- 自同態 (Endomorphism)
- 類型簽名 (Type Signatures)
Arity
函數參數的個數。來自于單詞 unary, binary, ternary 等等。這個單詞是由 -ary 與 -ity 兩個后綴拼接而成。例如,一個帶有兩個參數的函數被稱為二元函數或者它的 arity 是2。它也被那些更喜歡希臘詞根而非拉丁詞根的人稱為 dyadic 。同樣地,帶有可變數量參數的函數被稱為 variadic ,而二元函數只能帶兩個參數。
const sum = (a, b) => a + b
const arity = sum.length
console.log(arity) // 2
高階函數 (Higher-Order Function / HOF)
以函數為參數或/和返回值。
const filter = (predicate, xs) => xs.filter(predicate)
const is = (type) => (x) => Object(x) instanceof type
filter(is(Number), [0, '1', 2, null]) // 0, 2
偏函數 (Partial Function)
對原始函數預設參數作為一個新的函數。
// 創建偏函數,固定一些參數
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
也可以使用 Function.prototype.bind 實現偏函數。
const add1More = add3.bind(null, 2, 3)
偏函數應用通過對復雜的函數填充一部分數據來構成一個簡單的函數。柯里化通過偏函數實現。
柯里化 (Currying)
將一個多元函數轉變為一元函數的過程。 每當函數被調用時,它僅僅接收一個參數并且返回帶有一個參數的函數,直到傳遞完所有的參數。
const sum = (a, b) => a + b
const curriedSum = (a) => (b) => a + b
curriedSum(3)(4) // 7
const add2 = curriedSum(2)
add2(10) // 12
自動柯里化 (Auto Currying)
lodash , understore 和 ramda 有 curry 函數可以自動完成柯里化。
const add = (x, y) => x + y
const curriedAdd = _.curry(add)
curriedAdd(1, 2) // 3
curriedAdd(1)(2) // 3
curriedAdd(1) // (y) => 1 + y
進一步閱讀
函數組合 (Function Composing)
接收多個函數作為參數,從右到左,一個函數的輸入為另一個函數的輸出。
const compose = (f, g) => (a) => f(g(a)) // 定義
const floorAndToString = compose((val) => val.toString(), Math.floor) // 使用
floorAndToString(12.12) // '12'
Continuation
在一個程序執行的任意時刻,尚未執行的代碼稱為 Continuation。
const printAsString = (num) => console.log(`Given ${num}`)
const addOneAndContinue = (num, cc) => {
const result = num + 1
cc(result)
}
addOneAndContinue(2, printAsString) // 'Given 3'
Continuation 在異步編程中很常見,比如當程序需要接收到數據才能夠繼續執行。請求的響應通常作為代碼的剩余執行部分,一旦接收到數據,對數據的處理被作為 Continuation。
const continueProgramWith = (data) => {
// 繼續執行程序
}
readFileAsync('path/to/file', (err, response) => {
if (err) {
// 錯誤處理
return
}
continueProgramWith(response)
})
純函數 (Purity)
輸出僅由輸入決定,且不產生副作用。
const greet = (name) => `hello, ${name}`
greet('world')
以下代碼不是純函數:
window.name = 'Brianne'
const greet = () => `Hi, ${window.name}`
greet() // "Hi, Brianne"
以上示例中,函數依賴外部狀態。
let greeting
const greet = (name) => {
greeting = `Hi, ${name}`
}
greet('Brianne')
greeting // "Hi, Brianne"
以上實例中,函數修改了外部狀態。
副作用 (Side effects)
如果函數與外部可變狀態進行交互,則它是有副作用的。
const differentEveryTime = new Date()
console.log('IO is a side effect!')
冪等性 (Idempotent)
如果一個函數執行多次皆返回相同的結果,則它是冪等性的。
f(f(x)) ? f(x)
Math.abs(Math.abs(10))
sort(sort(sort([2, 1])))
Point-Free 風格 (Point-Free Style)
定義函數時,不顯式地指出函數所帶參數。這種風格通常需要柯里化或者高階函數。也叫 Tacit programming。
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))
incrementAll 識別并且使用了 numbers 參數,因此它不是 Point-Free 風格的。 incrementAll2 連接函數與值,并不提及它所使用的參數,因為它是 Point-Free 風格的。
Point-Free 風格的函數就像平常的賦值,不使用 function 或者 => 。
謂詞 (Predicate)
根據輸入返回 true 或 false。通常用在 Array.prototype.filter 的回調函數中。
const predicate = (a) => a > 2
;[1, 2, 3, 4].filter(predicate)
契約 (Contracts)
契約保證了函數或者表達式在運行時的行為。當違反契約時,將拋出一個錯誤。
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
Guarded Functions
TODO
范疇 (Category)
在范疇論中,范疇是指對象集合及它們之間的態射 (morphism)。在編程中,數據類型作為對象,函數作為態射。
一個有效的范疇遵從以下三個原則:
- 必有一個 identity 態射,使得 map 一個對象是它自身。 a 是范疇里的一個對象時,必有一個函數使 a -> a 。
- 態射必是可組合的。 a , b , c 是范疇里的對象, f 是態射 a -> b , g 是 b -> c 態射。 g(f(x)) 一定與 (g ● f)(x)
- 組合滿足結合律。 f ● (g ● h) 與 (f ● g) ● h 是等價的。
這些準則是非常抽象的,范疇論對與發現組合的新方法是偉大的。
進一步閱讀
值 (Value)
賦值給變量的值稱作 Value。
5
Object.freeze({name: 'John', age: 30})
;(a) => a
;[1]
undefined
常量 (Constant)
一旦定義不可重新賦值。
const five = 5
const john = Object.freeze({name: 'John', age: 30})
常量是的,因此它們可以被它們所代表的值替代而不影響結果。
對于以上兩個常量,以下語句總會返回 true。
john.age + five === ({name: 'John', age: 30}).age + (5)
函子 (Functor)
一個實現了map 函數的對象,map 會遍歷對象中的每個值并生成一個新的對象。遵守兩個準則
一致性 (Preserves identity)
object.map(x => x) ? object
組合性 (Composable)
object.map(compose(f, g)) ? object.map(g).map(f) // f, g 為任意函數
在 javascript 中一個常見的函子是 Array, 因為它遵守因子的兩個準則。
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)
Pointed Functor
一個實現了 of 函數的對象。
ES2015 添加了 Array.of ,使 Array 成為了 Pointed Functor。
Array.of(1)
Lift
TODO
引用透明性 (Referential Transparency)
一個表達式能夠被它的值替代而不改變程序的行為成為引用透明。
const greet = () => 'hello, world.'
Equational Reasoning
TODO
匿名函數 (Lambda)
匿名函數被視作一個值
;(function (a) {
return a + 1
})
;(a) => a + 1
匿名函數通常作為高階函數的參數
[1, 2].map((a) => a + 1)
可以把 Lambda 賦值給一個變量
const add1 = (a) => a + 1
Lambda Caculus
數學的一個分支,使用函數創造 通過計算模型
惰性求值 (Lazy evaluation)
按需求值機制,只有當需要計算所得值時才會計算。
const rand = function* () {
while (true) {
yield Math.random()
}
}
const randIter = rand()
randIter.next()
Monoid
一個對象擁有一個函數用來連接相同類型的對象。
數值加法是一個簡單的 Monoid
1 + 1 // 2
以上示例中,數值是對象而 + 是函數。
與另一個值結合而不會改變它的值必須存在,稱為 identity 。
加法的 identity 值為 0:
1 + 0 // 1
需要滿足結合律
1 + (2 + 3) === (1 + 2) + 3 // true
數組的結合也是 Monoid
;[1, 2].concat([3, 4])
identity 值為空數組
;[1, 2].concat([])
identity 與 compose 函數能夠組成 monoid
const identity = (a) => a
const compose = (f, g) => (x) => f(g(x))
foo 是只帶一個參數的任意函數
compose(foo, identity) ? compose(identity, foo) ? foo
Monad
擁有 of 和 chain 函數的對象。 chain 很像 map , 除了用來鋪平嵌套數據。
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(','))
在有些語言中, of 也稱為 return , chain 也稱為 flatmap 與 bind 。
Comonad
擁有 extract 與 extend 函數的對象。
const CoIdentity = (v) => ({
val: v,
extract () {
return this.val
},
extend (f) {
return CoIdentity(f(this))
}
})
CoIdentity(1).extract()
CoIdentity(1).extend(x => x.extract() + 1) # CoIdentity(2)
Applicative Functor
一個擁有 ap 函數的對象。
// 實現
Array.prototype.ap = function (xs) {
return this.reduce((acc, f) => acc.concat(xs.map(f)), [])
}
// 示例
;[(a) => a + 1].ap([1]) // [2]
如果你有兩個對象,并需要對他們的元素執行一個二元函數
// 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]
由此得到了一個函數數組,并且可以調用 ap 函數得到結果
partiallyAppliedAdds.ap(arg2) // [5, 6, 7, 8]
態射 (Morphism)
一個變形的函數。
自同態 (Endomorphism)
輸入輸出是相同類型的函數。
// uppercase :: String -> String
const uppercase = (str) => str.toUpperCase()
// decrement :: Number -> Number
const decrement = (x) => x - 1
同構 (Isomorphism)
不用類型對象的變形,保持結構并且不丟失數據。
例如,一個二維坐標既可以表示為數組 [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
擁有 equals 函數的對象。 equals 可以用來和其它對象比較。
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
半群 (Semigroup)
一個擁有 concat 函數的對象。 concat 可以連接相同類型的兩個對象。
;[1].concat([2]) // [1, 2]
Foldable
一個擁有 reduce 函數的對象。 reduce 可以把一種類型的對象轉化為另一種類型。
const sum = (list) => list.reduce((acc, val) => acc + val, 0)
sum([1, 2, 3]) // 6
Traversable
TODO
類型簽名 (Type Signatures)
通常 js 會在注釋中指出參數與返回值的類型。
// 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)
字符 a, b, c, d 表明參數可以是任意類型。以下版本的 map 的參數 f,把一種類型 a 的數組轉化為另一種類型 b 的數組。
// map :: (a -> b) -> [a] -> [b]
const map = (f) => (list) => list.map(f)
聯合類型 (Union Type)
連接不同的數據類型。
js 沒有靜態類型,我們假設一個數據類型是 NumOrString 用來對 Number 與 String 兩種類型求和。
js 中可以對數值或字符串使用 + 操作符,因此我們可以使用這個新類型去描述輸入輸出。
// 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"
聯合類型又稱為代數類型 algebraic types,tagged union 或者 sum type。
這里有一些 js 庫可以幫助我們定義和使用聯合類型。
Product type
用一種你可能更熟悉的方式把數據類型聯合起來
// point :: (Number, Number) -> {x: Number, y: Number}
const point = (x, y) => ({x: x, y: y})
Option
Option 是一種聯合類型,它有兩種情況, Some 或者 None 。
// 定義
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)
在其它的一些地方, Option 也稱為 Maybe , Some 也稱為 Just , None 也稱為 Nothing 。
來自:https://juejin.im/entry/58c02176128fe1006b1c5792