Curry,Decorator 與 HOC

從 curry -> decorator -> HOC 的漫漫長路。

Curry

咖哩

天冷吃咖哩可有效去寒

可惜本文要聊的 curry 不是吃的,更不是打籃球的。

curried function 是指函式一次接受一個參數,利用 closure(閉包)特性,將它們存放在另一個函式中並回傳,直到參數蒐集完畢才做計算,目的是將大問題切割成小問題,以便逐步解決。

範例 1

如下,將兩數相乘切分成兩個步驟來傳入參數,首先使用一個函數 multiply 回傳記住第一個傳入了參數 5 的函數,並指定給變數 multiplyTwoNumbers;再來,從 multiplyTwoNumbers 傳入第二個參數 4,並將結果指定給變數 result,印出 result 而得到 20。

const multiply = (x) => {
  return (y) => {
    return x * y;
  };
};

const multiplyTwoNumbers = multiply(5);
const result = multiplyTwoNumbers(4);
console.log(result); // 20

也可以這麼寫

multiply(5)(4); // 20

此時 x 為 5,y 為 4,得到結果 20。

範例 2

改寫上例,將參數蒐集器(arguments collector)與兩數相乘的函式(multiply)獨立出來。

// arguments collector
const curry = (fn) => (...args) => {
  return fn.bind(null, ...args);
};

// multiply two numbers
const multiply = (x, y) => {
  return x * y;
};

curried 是個 curried function。

const curried = curry(multiply);

curried(3, 4); // (x, y) => { return x * y; }

此時 x 為 3,y 為 4,得到結果 12。

curried(3, 4)(); // 12
curried(3)(4); // 12

使用 curried function 的好處是能簡化參數的處理,若無法在第一時間就得到所有需要的參數,就可這樣來批次處理;也因為可批次處理,因此可將程式碼拆解成更細的片段,有助可讀性、彈性、方便重複利用。

備註:currying(柯里化)是指「將一個接受 n 個參數的函式,轉變成 n 個只接受一個參數的函式」的過程,可參考這裡

除了傳數字進去,也可以傳各式各樣的資料結構。下面要提到的就是其中一種應用,利用柯里化的原理將元件傳到函式中,包裝元件而能新增屬性,也就是 decorator。

Decorator 裝飾器

decorator(裝飾器)即是柯里化的應用,如下所示,函式 sayHello 先傳入字串 name,再傳入元件 cmp,即可為元件新增屬性 intro。

範例 3

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

const App = () => {
  return (
    <div className='App'>
      <HelloWorld />
    </div>
  );
};

const sayHello = (name) => {
  return (cmp) => {
    cmp.intro = `Hello, I am ${name}`;
    console.log(cmp.intro);
    return cmp;
  };
};

class HelloWorld extends Component {
  render() {
    return <div>Hello World!</div>;
  }
}

const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);
sayHello('John')(HelloWorld);

執行結果,依序將字串 John 與元件 HelloWorld 帶入函式 sayHello 中,設定元件的屬性 intro,最後印出字串。

Hello, I am John

範例 4

使用多個 decorator 也是可以的,在這裡使用了兩個 decorator-sayHi 與 sayHello 來包裝元件 HelloWorld。

const App = () => {
  return (
    <div className='App'>
      <HelloWorld />
    </div>
  );
};

const sayHi = (name) => {
  return (cmp) => {
    cmp.intro = `Hi, I am ${name}`;
    console.log(cmp.intro);
    return cmp;
  };
};

const sayHello = (name) => {
  return (cmp) => {
    cmp.greeting = `Hello, I am ${name}`;
    console.log(cmp.greeting);
    return cmp;
  };
};

class HelloWorld extends Component {
  render() {
    return <div>Hello World!</div>;
  }
}

const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);
sayHello('Patty')(sayHi('John')(HelloWorld));

執行結果。

Hi, I am John
Hello, I am Patty

範例 5

可用新式語法 @ 來使用 decorator。注意,目前 decorator 尚在 Stage 2 階段,並未正式全面支援,因此若想使用就必須安裝 babel-plugin-transform-decorators-legacy 做轉譯。如下所示,sayHi 是個 decorator,為 HelloWorld 新增 intro 的屬性。

const App = () => {
  return (
    <div className='App'>
      <HelloWorld />
    </div>
  );
};

const sayHi = (cmp) => {
  cmp.intro = 'Hi';
  return cmp;
};

@sayHi
class HelloWorld extends Component {
  render() {
    return <div>Hello World!</div>;
  }
}

const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);
console.log(HelloWorld.intro);

執行結果,將元件 HelloWorld 的屬性 intro 所設定的字串印出來。

Hi;

範例 6

decorator 也可以代入參數喔!如下所示,sayHi 可傳入一個字串當成打招呼時用的名字。

const App = () => {
  return (
    <div className='App'>
      <HelloWorld />
    </div>
  );
};

const sayHi = (name) => {
  return (cmp) => {
    cmp.intro = `Hi, I am ${name}`;
    console.log(cmp.intro);
    return cmp;
  };
};

@sayHi('John')
class HelloWorld extends Component {
  render() {
    return <div>Hello World!</div>;
  }
}

const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);

執行結果,將 John 帶入 sayHi 中,設定給屬性 intro,最後印出來。

Hi, I am John

範例 7

使用多個 decorator。

const App = () => {
  return (
    <div className='App'>
      <HelloWorld />
    </div>
  );
};

const sayHi = (name) => {
  return (cmp) => {
    cmp.intro = `Hi, I am ${name}`;
    console.log(cmp.intro);
    return cmp;
  };
};

const sayHello = (name) => {
  return (cmp) => {
    cmp.greeting = `Hello, I am ${name}`;
    console.log(cmp.greeting);
    return cmp;
  };
};

@sayHello('Patty')
@sayHi('John')
class HelloWorld extends Component {
  render() {
    return <div>Hello World!</div>;
  }
}

const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);

執行結果。

Hi, I am John
Hello, I am Patty

react-decoration

react-decoration 是一個提供 React.js 元件的方法可用的 decorator 的函式庫。舉個簡單的例子來看吧,假設我們希望能印出傳入方法的參數,就可使用 @log 來做這件事情,可參考官方原始碼範例文件

範例 8

範例如下,點擊按鈕「View log!」時,印出傳入 onClickHandler 的參數。

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { log } from 'react-decoration';

const App = () => {
  return (
    <div className='App'>
      <HelloWorld />
    </div>
  );
};

const sayHi = (name) => {
  return (cmp) => {
    cmp.intro = `Hi, I am ${name}`;
    console.log(cmp.intro);
    return cmp;
  };
};

const sayHello = (name) => {
  return (cmp) => {
    cmp.greeting = `Hello, I am ${name}`;
    console.log(cmp.greeting);
    return cmp;
  };
};

class HelloWorld extends Component {
  constructor(props) {
    super(props);

    this.onClickHandler = this.onClickHandler.bind(this);
  }

  @log
  onClickHandler(e) {
    // ...
  }

  render() {
    return (
      <div>
        Hello World!
        <button onClick={this.onClickHandler}>View log!</button>
      </div>
    );
  }
}

const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);
sayHello('Patty')(sayHi('John')(HelloWorld));

點擊按鈕「View log!」後的執行結果,可看到「Calling function “onClickHandler” with params…」印出傳入 onClickHandler 的參數。

Hi, I am John
Hello, I am Patty
Calling function "onClickHandler" with params:  Proxy {dispatchConfig: {}, _targetInst: FiberNode, nativeEvent: MouseEvent, type: "click", target: button, …}

似乎 HOC 的撰寫方式與 decorator 有 87 分像!沒錯,HOC 其實就是利用先前提到的 curry、decorator 的觀念,或可說是我們所熟知的 higher-order function,只是把輸入與輸出的參數從函式改為元件而已。因此,在實作上,HOC 即是將元件當成參數傳入函式,再將經過 decorator 包裝後的元件輸出。

HOC

重構 JavaScript (Refactoring JavaScript: Turning Bad Code Into Good Code)

截圖自天瓏網路書店。

「沒事多重構,多重構沒事」,趕快用 HOC 整理程式碼吧。

HOC(Higher-Order Component)是指分離元件的表現層與展示層,將表現層製成 pure component(或稱 presentational component),而在 HOC 放置核心邏輯、提取資料的 API、處理資料的各種方法等,也就是 container component,最後將表現層元件當成參數傳給 HOC,輸出包裝後的元件。關於如何實作 HOC 可參考這裡

這麼做的好處是「關注點分離」,分開 UI 與邏輯兩部份而讓元件更好維護,並且可避免每次都重新打造相似的輪子,而達到省時省力、可重用、更好的模組化的目的。

常用的 react-redux 的 connect 就是一個例子,connect 將 reducer state 包裝成 prop 與 action 傳給個別元件。

import React, { Component } from 'react';
import { connect } from 'react-redux';

class HelloWorld extends Component {
  // ...
}

export default connect(mapStateToProps)(HelloWorld);

和上面提到的 decorator 觀念相同,因此可將 HOC 改寫為 decorator 的方式。

import React, { Component } from 'react';
import { connect } from 'react-redux';

@connect(mapStateToProps)
class HelloWorld extends Component {
  // ...
}

export default HelloWorld;

同樣的,使用多個 HOC 也是可以的,方法就如同前面提到的 decorator 一樣。

recompose

recompose 提供給 React.js 許多實用的 HOC,舉個簡單的例子來說,如前面提到,我們用了很多個 HOC 來包裝元件,看得眼花撩亂的,因此可用 compose 來做簡化,可參考官網範例

範例 9

如前面範例 4 所示,我們使用了多個 decorator 來包裝元件 HelloWorld,使其能擁有 sayHi 和 sayHello 的功能。

原本的寫法。

const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);
sayHello('Patty')(sayHi('John')(HelloWorld));

改進後的寫法,利用 compose 來為多個 HOC 的組合做簡化,看起來清爽多了。

const enhance = compose(sayHello('Patty'), sayHi('John'));

const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);
enhance(HelloWorld);

完整程式碼。

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { compose } from 'recompose';

const App = () => {
  return (
    <div className='App'>
      <HelloWorld />
    </div>
  );
};

const sayHi = (name) => {
  return (cmp) => {
    cmp.intro = `Hi, I am ${name}`;
    console.log(cmp.intro);
    return cmp;
  };
};

const sayHello = (name) => {
  return (cmp) => {
    cmp.greeting = `Hello, I am ${name}`;
    console.log(cmp.greeting);
    return cmp;
  };
};

class HelloWorld extends Component {
  render() {
    return <div>Hello World!</div>;
  }
}

// 利用 `compose` 來為多個 HOC 的組合做簡化
const enhance = compose(sayHello('Patty'), sayHi('John'));

const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);
enhance(HelloWorld);

執行結果。

Hi, I am John
Hello, I am Patty

雖然 compose 是用於為多個 HOC 的組合做簡化,但我個人還是比較喜歡用 decorator @

@sayHello('Patty')
@sayHi('John')
class HelloWorld extends Component {
  render() {
    return <div>Hello World!</div>;
  }
}

const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);

參考資料


javascript react.js functional programming