【譯】函數式編程-柯里化和函數合成

原文:medium.com/javascript-…
做者:Eric Elliott
翻譯:前端小白javascript

注意:這篇文章是 "Composing Software" 學習函數式編程 這個系列(如今已出版爲一本書)裏面的一部分。前端

什麼是函數柯里化

一個被柯里化的函數是一個本來接受多個參數,轉變成接受單一參數的函數。給定一個有3個參數的函數,將他柯里化後,它將接受一個參數並返回一個函數,這個返回的函數又接受下一個參數,接下來又返回一個函數接受第三個參數,最後一個函數返回接受全部參數後函數運行的結果。java

你可使用數量不定的參數來實現柯里化,好比給定一個函數接受兩個參數 ab,將函數柯里化後返回兩個參數的和shell

// add = a => b => Number
const add = a => b => a + b;
複製代碼

咱們須要調用這兩個函數來使用 add 函數,調用函數的語法就是在函數引用後面加上 (),當一個函數返回另外一個函數時,能夠經過添加一組額外的括號當即調用返回的函數:編程

const result = add(2)(3); // => 5
複製代碼

首先,這個函數接受 a 參數,而後返回一個新函數,新函數接受 b 參數,而後返回 ab的和,每次都只接受一個參數。若是函數有更多的參數,那就簡單地繼續返回新函數,直到全部的參數都挨個被接收,而後程序運行完成。數組

add 函數接受一個參數,而後返回了它自身的一部分,這部分中有個變量 afixed 在該函數的閉包中,閉包就是某個函數有個和它捆綁在一塊兒的一個詞法做用域,閉包是在函數建立期間的運行時創建的,fixed 意味着閉包裏面的變量只會在閉包環境中被賦值。bash

上面例子中的括號表示函數被調用:add 函數被調用,同時傳入了參數 2,返回一個部分應用的函數,這時 a 的值被固定爲 2。這時候返回值不會被賦值給某個變量或者以其餘方式去使用,咱們立刻又會將 3 傳入,並調用這個返回的函數,如今程序結束,返回結果 5閉包

什麼是部分應用(偏函數)

部分應用是一個應用了一個或者多個參數的函數,沒有接受所有參數。換句話說,部分應用就是一個函數,這個函數有一些參數被固化在它的閉包做用域範圍內。一個函數的某些參數是固定的,咱們稱之爲部分應用app

區別

部分應用能夠根據須要一次使用任意多或任意少的參數,而柯里化的函數老是返回一個一元函數,即只接受一個參數的函數。函數式編程

全部的柯里化函數都返回部分應用,但不是全部的部分應用都是柯里化函數的結果。

柯里化函數的一元需求是一個很重要的特性。

什麼是 point-free style

point-free style 是一種編程風格,函數在定義時,不要對函數的參數進行引用,咱們看看下面的函數定義:

function foo (/* 這裏聲明參數*/) {
  // ...
}
const foo = (/* 這裏聲明參數 */) => // ...
const foo = function (/* 這裏聲明參數 */) {
  // ...
}
複製代碼

如何在不引用所需參數的狀況下用JavaScript定義函數?咱們不能使用 function 關鍵字,也不能使用箭頭函數(=>),由於這些都要求正式的參數聲明(會引用該參數)。因此咱們要作的是調用一個會返回函數的函數。

使用 point-free style 建立一個函數,該函數會將你傳入的任何數字加1,記住,咱們已經有了一個名爲 add 的函數,它接受一個數字並返回一個部分應用的函數,其第一個參數被固化爲傳入的任何值。咱們可使用它來建立一個 inc()

// inc = n => Number
// Adds 1 to any number.
const inc = add(1);
inc(3); // => 4
複製代碼

做爲一種廣泛化和專門化機制,這頗有趣,返回的函數與 add 這種通用版比較,至關於一種專門的定製版,咱們可使用 add 來創造更多不一樣的版本

const inc10 = add(10);
const inc20 = add(20);
inc10(3); // => 13
inc20(3); // => 23
複製代碼

固然,他們都有本身的閉包做用域範圍(閉包是在函數建立時產生的,當add() 函數被建立時),因此原始的 inc() 能夠繼續工做

inc(3) // 4
複製代碼

當咱們調用函數 add(1) 時就建立了 inc() 函數,在這個被返回的函數中,add() 函數裏面的 a參數值被固定爲1,被返回的函數賦值給 inc,當咱們調用 inc(3) 時,add() 函數裏面的 b 參數就被實參 3 替代,整個程序結束,返回 13 的和。

全部柯里化函數都是高階函數的一種形式,它容許爲手頭的特定用例建立一個原始函數的特定版本。

爲何須要柯里化

函數柯里化在函數組合上下文中特別有用

在代數中,給定兩個函數 gf

g: a -> b
f: b -> c
複製代碼

你能夠將他們組合成一個新函數,從 a 直接到 ch 函數

// 代數定義,藉助 Haskell 中的 `.` 組合運算符
h: a -> c
h = f . g = f(g(x))
複製代碼

在Javascript中:

const g = n => n + 1;
const f = n => n * 2;
const h = x => f(g(x));
h(20); //=> 42
複製代碼

代數定義中:

f . g = f(g(x))
複製代碼

轉換爲Javascript語言:

const compose = (f, g) => x => f(g(x));
複製代碼

可是這一次只能組合兩個函數。在代數中,能夠這樣寫:

f . g . h
複製代碼

咱們能夠編寫一個函數來組合任意多個函數,換句話說,compose() 建立一個函數管道,其中一個函數的輸出會鏈接到下一個函數的輸入。

我一般喜歡這麼寫:

const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x);
複製代碼

這種寫法接受任意數量的函數並返回一個接受初始值的函數,而後使用 reduceRight()fns 中的 f 從右到左的迭代,並調用,獲得最後累積的值,在這個函數裏面,咱們用累加器積累的 ycompose() 這個函數返回的函數的返回值。

如今咱們能夠這樣組合:

const g = n => n + 1;
const f = n => n * 2;
// 將 `x => f(g(x))` 用 `compose(f, g)` 替換
const h = compose(f, g);
h(20); //=> 42
複製代碼

追蹤

函數組合使用了 point-free style 使代碼很是簡潔、可讀。可是也爲代碼調試帶來了不便,若是你想監測兩個函數之間的值,該怎麼作?trace() 是一個十分方便的工具,可讓你作到這一點。 它採用了函數柯里化的形式:

const trace = label => value => {
  console.log(`${ label }: ${ value }`);
  return value;
};
複製代碼

如今咱們能監測函數管道:

const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x);
const trace = label => value => {
  console.log(`${ label }: ${ value }`);
  return value;
};
const g = n => n + 1;
const f = n => n * 2;
/* 注意函數調用順序是從下至上 */
const h = compose(
  trace('after f'),
  f,
  trace('after g'),
  g
);
h(20);
/* after g: 21 after f: 42 */
複製代碼

compose() 是一個不錯的工具,可是當咱們須要組合超過兩個函數時,若是咱們能按從上到下的順序閱讀,有時會很方便。咱們能夠經過反轉函數的調用順序來作到這一點,還有一個名爲 pipe() 的工具函數,它以相反的順序組合咱們的函數。

const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
複製代碼

如今上面的代碼能夠這樣寫:

const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
const trace = label => value => {
  console.log(`${ label }: ${ value }`);
  return value;
};
const g = n => n + 1;
const f = n => n * 2;
/* 如今函數調用順序時是從上到下 */
const h = pipe(
  g,
  trace('after g'),
  f,
  trace('after f'),
);
h(20);
/* after g: 21 after f: 42 */
複製代碼

柯里化和函數組合結合

即便沒有與函數組合結合起來,柯里化也是一個實用的抽象功能,咱們可使用它來專門化一個函數。例如,柯里化版本的 map() 函數能夠專門用於作許多不一樣的事情:

const map = fn => mappable => mappable.map(fn);
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
const log = (...args) => console.log(...args);
const arr = [1, 2, 3, 4];
const isEven = n => n % 2 === 0;
const stripe = n => isEven(n) ? 'dark' : 'light';
const stripeAll = map(stripe);
const striped = stripeAll(arr); 
log(striped);
// => ["light", "dark", "light", "dark"]
const double = n => n * 2;
const doubleAll = map(double);
const doubled = doubleAll(arr);
log(doubled);
// => [2, 4, 6, 8]
複製代碼

可是柯里化真正的優點是它簡化了函數組合,一個函數能夠接受任意數量的輸入,但只有一個輸出,爲了使函數可組合,輸出類型必須與預期的輸入類型一致:

f: a => b
g:      b => c
h: a    =>   c
複製代碼

若是上面的 g 函數但願接受兩個參數,f 函數的輸出就與 g 函數的輸入不一致了:

f: a => b
g:     (x, b) => c
h: a    =>   c
複製代碼

在這種狀況下,咱們怎樣能夠將 x 傳給 g,答案就是將 g 柯里化

記住,柯里化函數的定義就是一個函數,它一次接受多個參數,取第一個參數並返回一系列函數,每一個函數取下一個參數,直到收集到全部參數。

定義中的關鍵字詞是 一次接受一個,柯里化函數對於函數組合來講很是方便,緣由就是它們將指望接受多個參數的函數轉換爲能夠接受單個參數的函數,使他們能夠應用在函數組合管道中

之前面的 trace() 函數爲例

const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
const trace = label => value => {
  console.log(`${ label }: ${ value }`);
  return value;
};
const g = n => n + 1;
const f = n => n * 2;
const h = pipe(
  g,
  trace('after g'),
  f,
  trace('after f'),
);
h(20);
/* after g: 21 after f: 42 */
複製代碼

trace() 定義了兩個參數,可是一次只能接受一個,這樣咱們能夠在函數體裏面來專門化 trace() 函數,若是它沒喲被柯里化,咱們就不能這樣使用,咱們的整個函數組合管道看起來像這樣

const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
const trace = (label, value) => {
  console.log(`${ label }: ${ value }`);
  return value;
};
const g = n => n + 1;
const f = n => n * 2;
const h = pipe(
  g,
  // the trace() calls are no longer point-free,
  // introducing the intermediary variable, `x`.
  x => trace('after g', x),
  f,
  x => trace('after f', x),
);
h(20);
複製代碼

可是簡單的柯里化一個函數是不夠的。還須要將這個函數專門化並確保函數指望的參數的順序,若是咱們再次將 trace() 柯里化,可是翻轉參數順序,會發生什麼。

const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
const trace = value => label => {
  console.log(`${ label }: ${ value }`);
  return value;
};
const g = n => n + 1;
const f = n => n * 2;
const h = pipe(
  g,
  // the trace() calls can't be point-free,
  // because arguments are expected in the wrong order.
  x => trace(x)('after g'),
  f,
  x => trace(x)('after f'),
);
h(20);
複製代碼

若是你以爲很難理解,您可使用一個 flip() 的函數來解決這個問題,該函數只翻轉兩個參數的順序

const flip = fn => a => b => fn(b)(a);
複製代碼

如今咱們來建立一個 flippedTrace() 函數

const flippedTrace = flip(trace);
複製代碼

像這樣使用

const flip = fn => a => b => fn(b)(a);
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
const trace = value => label => {
  console.log(`${ label }: ${ value }`);
  return value;
};
const flippedTrace = flip(trace);
const g = n => n + 1;
const f = n => n * 2;
const h = pipe(
  g,
  flippedTrace('after g'),
  f,
  flippedTrace('after f'),
);
h(20);
複製代碼

但更好的方式是先正確編寫函數,這種風格有人稱爲 data last,意思就是應該將專門化的參數放在前面,而後將函數使用到的數據放在最後,以函數的原始形式

const trace = label => value => {
  console.log(`${ label }: ${ value }`);
  return value;
};
複製代碼

每一個 trace() 對應一個 label,這是一個用於函數管道里面的專門化的 trace() 函數,label 的值在 trace() 返回的偏函數中就已經被肯定了

const trace = label => value => {
  console.log(`${ label }: ${ value }`);
  return value;
};
const traceAfterG = trace('after g');
複製代碼

上面的代碼和如下等同:

const traceAfterG = value => {
  const label = 'after g';
  console.log(`${ label }: ${ value }`);
  return value;
};
複製代碼

若是咱們將 trace('after g')traceAfterG 替換,意思是同樣的:

const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
const trace = label => value => {
  console.log(`${ label }: ${ value }`);
  return value;
};
// The curried version of trace()
// saves us from writing all this code...
const traceAfterG = value => {
  const label = 'after g';
  console.log(`${ label }: ${ value }`);
  return value;
};
const g = n => n + 1;
const f = n => n * 2;
const h = pipe(
  g,
  traceAfterG,
  f,
  trace('after f'),
);
h(20);
複製代碼

結論

柯里化函數是一個本來接受多個參數,轉變成接受單一參數的函數,經過第一個參數,並返回一系列函數,接着又取下一個參數,直到全部的參數被用盡,程序結束,返回結果。

部分應用(偏函數是指一個函數,只接受了部分,不是所有參數,那些已經被接受了的參數稱爲固定參數

Point-free style 是一種編程風格,函數在定義時,不要對函數的參數進行引用,一般咱們經過調用一個返回值爲函數的函數來建立一個 point-free 函數,好比柯里化函數。

柯里化函數對於函數組合很是有用,由於它容許你輕鬆地將一個n元函數轉換爲函數組合管道所需的一元函數形式:函數管道中的函數必須只接受一個參數。

Data last 函數對於函數組合來講很方便,他們能夠很容易的使用 point-free style 形式

相關文章
相關標籤/搜索