カリー化はナンの役に立つのか

2f374df92882247e5985413dad8b39b9?s=47 fsubal
February 08, 2019

 カリー化はナンの役に立つのか

昔ニコナレにアップロードしていたピクシブ社内勉強会資料です。

2f374df92882247e5985413dad8b39b9?s=128

fsubal

February 08, 2019
Tweet

Transcript

  1. カリー化は ナンの役に立つのか Web フロントエンド 編 pixiv Inc. f_subal 2018/12/07

  2. 2 誰 • pixivFACTORY フロントエンド • React と Redux と

    SVG • 将来の夢は Single SVG Application です @f_subal
  3. みなさん カリー化してますか 3

  4. 私は React + TS で 息をするように使ってます 4

  5. 「りろんはしってる」けど 5

  6. 「どう嬉しいのかわからない」 と言われがち 6

  7. • カリー化を用いるとこんなことができる ◦ 読みやすい高階関数の building block をつくる ◦ 関数で DI

    する ◦ 時間とともに変化する関数をつくる 7 話すこと
  8. カリー化 (currying) 8

  9. • n 引数関数を、n 回適用可能な 1 引数の連なりに変形すること • 例 ◦ (a:

    number, b: number) => Math.max(a, b) が ◦ (a: number) => (b: number) => Math.max(a, b) になる ◦ a を渡すと、「b を渡すと number が返る関数」が返る 9 カリー化とは
  10.   // before   function getMax(a: number, b: number) { return Math.max(a,

    b) }   // after (curried)   function getMax(a: number) { return function (b: number) { return Math.max(a, b) } } 11
  11. へー (何が嬉しいのかわからん) 11

  12. たとえば .filter と合わせる 12

  13. 例題) number[] から 3 より 大きい要素だけを抽出せよ 13

  14.   // before   function isLarger(a: number, b: number) { return a

    > b }   // with before   const largerThanThrees = inputs.filter(function (n) { return isLarger(n, 3) }) 11
  15. これでも悪くないが 15

  16.   // after (curried)   function isLarger(a: number) { return function (b:

    number) { return a < b } } // with after   const largerThanThrees = inputs.filter(isLarger(3)) 11
  17.   // after (curried)   function isLargerThan(a: number) { return function (b:

    number) { return a < b } } // with after   const largerThanThrees = inputs.filter(isLargerThan(3)) 11
  18.   // after (curried)   function isLargerThan(a: number) { return function (b:

    number) { return a < b } } // with after   const largerThanThrees = inputs.filter(isLargerThan(3)) 11
  19. いいじゃん 19

  20. • .map .filter など、JS は高階関数 API を多用する • すると、高階関数にビジネスロジックとして意味のあるパターンが現れる ◦

    .filter(isPremiumUser) とか ◦ .filter(isOwnedBy(user)) とか • こういう関数を切り出す際、カリー化しておくと便利 ◦ .map や .filter にドメインロジックが表現される ◦ 厳密に 1 引数ずつにしなくても「関数をかえす関数」にするだけでも便利 20 高階関数 の building block
  21. • 多言語出身の人は .filter(isPremiumUser) より .selectPremiumUser() と書きたがる • A. 諦めろ ◦

    経験上、クラスインスタンスにマッピングするメリットが、デメリットを上回ることはほぼ ない ◦ フロントエンドの殆どは「ユーザー入力を JSON に相互変換する処理」に過ぎない( 燃える発言 ) ◦ プレーンな関数に表現力を与える方向で議論したほうが有益( TypeScript はそれが 得意 ) 21 Q. オブジェクト指向すれば?
  22. 22

  23. 23

  24. 部分適用 (partial application) 24

  25. • 関数の引数の一部だけ、予めある値を束縛しておくこと ◦ 例: (a, b) => a + b

    があるとき、b=1 を部分適用すると increment 関数になる • カリー化された関数に対し、予め 1回(あるいは数回)呼んでおくことでも実現できる ◦ f が a => b => a + b のとき、f(1) すると b => b + 1 になる ◦ isOwnedBy(user) に currentUser を適用すると isMine(item) が作れる • カリー化と部分適用はよく混同される( 別の概念です ) 25 部分適用とは
  26. 部分適用したものを使いまわす 26

  27. たとえば単位換算 27

  28. 例題 1) 100mm の直線が 1181px で表示されてる (≒ 300dpi) この時カーソルを 30px

    動かすと、 何 mm 動いたことになる? 28
  29. 29

  30. とりあえず カリー化無しで 書いてみる 30

  31.   // without currying // 1 inch === 25.4 mm   function

    px2mm(px: number, dpi: number) { return px / dpi * 25.4 }   const dpi = 1181 / 100 * 25.4   const mm = px2mm(30, dpi) 11
  32. これでもいい、が… 32

  33. 例題 2) 実はいま 1181 px で 表示されてるのはたまたまで、 100mm の原稿が window

    の resize のたびに いろんな大きさ (px) になると仮定してください 33
  34. 例題 2) 実はいま 1181 px で 表示されてるのはたまたまで、 100mm の原稿が window

    の resize のたびに いろんな大きさ (px) になると仮定してください 34
  35. 11

  36. • (px, dpi) => mm という関数のうち、dpi が変わる場合がある ◦ dpi は「この

    mm がこの px で表示されている」という事実から決定される • dpi は window をリサイズしない限り変わらない • 普通に扱う場合は、dpi を固定した状態で、px → mm 換算が行えると嬉しい 36 つまり…
  37. • (px, dpi) => mm という関数を dpi => px =>

    mm という形式で表すとどうなるか ◦ makePx2Mm に dpi を部分適用すると px2mm ができる • window.resize のたびに makePx2Mm(dpi) を実行すると… ◦ 「dpi をある値で固定したときの px → mm 換算関数」ができる!! 37 そこで部分適用ですよ
  38. カリー化して解いてみる 38

  39.   // curried! // 1 inch === 25.4 mm   function makePx2mm(dpi:

    number) { return function (px: number) { return px / dpi * 25.4 } } 11
  40.   // in React Component class   public componentDidMount() { document.addEventListener('resize', ()

    => { this.setState({ currentDpi: ... }) }) } get px2mm() { return makePx2mm(this.state.currentDpi) } 11
  41.   // with Hooks (>= v16.8.0)   const [width, height] = useWindowSize()

      const currentDpi = calcDpi(...)   const px2mm = makePx2mm(currentDpi) 11
  42. お分かりいただけただろうか 42

  43. • ある時点において px ➔ mm はこういう式だが、別の時点では違う式になる • 外部要因( ここでは dpi

    )を注入して関数を組み立てるような設計にしたい ◦ dpi は更に window size に依存する 43 時間経過によって変動する式
  44. • 時間経過によって変化する対象( UI )をクラス / 関数で表す • そこに内部状態( React の

    state )を閉じ込める • カリー化された関数に、内部状態を適用して式を組み立てる • あとはそれを子に引き回す( React なら Context API と合わせても良い ) ◦ 子は単に this.props.px2mm() すればよい(親の dpi は知らなくて良い) • 時間経過によって変わる関数式を使い回せて便利 44 Stateful な対象と合わせる
  45. ところで 45

  46. • その通り ◦ メソッドが1個しかないクラスに対する DI みたいなものだと思って • むしろ自分は DI をクラスで部分適用するものと見ている

    ◦ new Service(arg).call(hoge) はカリー化の OOP 的解釈 ◦ 関数だと呼出が 2 段以上続くケースも作りやすい ( service(arg)(hoge)(...) ) 46 これって DI みたいなもの?
  47. • Q. 高階関数はともかくさ〜、if 文が ↓ みたいになるのイケてなくない? ◦ if ( isOwnedBy(user)(item)

    ) { • A. まぁそうですね ( uncurry しなければ ) • いま ECMAScript に提案中の pipeline 演算子がきたら ↓ みたいに書けるので辛抱してく ださい ◦ if ( item |> isOwnedBy(user) ) { 47 余談: Pipeline Operator
  48. • カリー化を用いるとこんなことができる ◦ 読みやすい高階関数の building block をつくる ◦ 関数で DI

    する ◦ 時間とともに変化する関数をつくる 48 まとめ