Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

2 誰 ● pixivFACTORY フロントエンド ● React と Redux と SVG ● 将来の夢は Single SVG Application です @f_subal

Slide 3

Slide 3 text

みなさん カリー化してますか 3

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

● カリー化を用いるとこんなことができる ○ 読みやすい高階関数の building block をつくる ○ 関数で DI する ○ 時間とともに変化する関数をつくる 7 話すこと

Slide 8

Slide 8 text

カリー化 (currying) 8

Slide 9

Slide 9 text

● n 引数関数を、n 回適用可能な 1 引数の連なりに変形すること ● 例 ○ (a: number, b: number) => Math.max(a, b) が ○ (a: number) => (b: number) => Math.max(a, b) になる ○ a を渡すと、「b を渡すと number が返る関数」が返る 9 カリー化とは

Slide 10

Slide 10 text

  // 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

Slide 11

Slide 11 text

へー (何が嬉しいのかわからん) 11

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

  // before   function isLarger(a: number, b: number) { return a > b }   // with before   const largerThanThrees = inputs.filter(function (n) { return isLarger(n, 3) }) 11

Slide 15

Slide 15 text

これでも悪くないが 15

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

いいじゃん 19

Slide 20

Slide 20 text

● .map .filter など、JS は高階関数 API を多用する ● すると、高階関数にビジネスロジックとして意味のあるパターンが現れる ○ .filter(isPremiumUser) とか ○ .filter(isOwnedBy(user)) とか ● こういう関数を切り出す際、カリー化しておくと便利 ○ .map や .filter にドメインロジックが表現される ○ 厳密に 1 引数ずつにしなくても「関数をかえす関数」にするだけでも便利 20 高階関数 の building block

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

22

Slide 23

Slide 23 text

23

Slide 24

Slide 24 text

部分適用 (partial application) 24

Slide 25

Slide 25 text

● 関数の引数の一部だけ、予めある値を束縛しておくこと ○ 例: (a, b) => a + b があるとき、b=1 を部分適用すると increment 関数になる ● カリー化された関数に対し、予め 1回(あるいは数回)呼んでおくことでも実現できる ○ f が a => b => a + b のとき、f(1) すると b => b + 1 になる ○ isOwnedBy(user) に currentUser を適用すると isMine(item) が作れる ● カリー化と部分適用はよく混同される( 別の概念です ) 25 部分適用とは

Slide 26

Slide 26 text

部分適用したものを使いまわす 26

Slide 27

Slide 27 text

たとえば単位換算 27

Slide 28

Slide 28 text

例題 1) 100mm の直線が 1181px で表示されてる (≒ 300dpi) この時カーソルを 30px 動かすと、 何 mm 動いたことになる? 28

Slide 29

Slide 29 text

29

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

  // 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

Slide 32

Slide 32 text

これでもいい、が… 32

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

11

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

● (px, dpi) => mm という関数を dpi => px => mm という形式で表すとどうなるか ○ makePx2Mm に dpi を部分適用すると px2mm ができる ● window.resize のたびに makePx2Mm(dpi) を実行すると… ○ 「dpi をある値で固定したときの px → mm 換算関数」ができる!! 37 そこで部分適用ですよ

Slide 38

Slide 38 text

カリー化して解いてみる 38

Slide 39

Slide 39 text

  // curried! // 1 inch === 25.4 mm   function makePx2mm(dpi: number) { return function (px: number) { return px / dpi * 25.4 } } 11

Slide 40

Slide 40 text

  // in React Component class   public componentDidMount() { document.addEventListener('resize', () => { this.setState({ currentDpi: ... }) }) } get px2mm() { return makePx2mm(this.state.currentDpi) } 11

Slide 41

Slide 41 text

  // with Hooks (>= v16.8.0)   const [width, height] = useWindowSize()   const currentDpi = calcDpi(...)   const px2mm = makePx2mm(currentDpi) 11

Slide 42

Slide 42 text

お分かりいただけただろうか 42

Slide 43

Slide 43 text

● ある時点において px ➔ mm はこういう式だが、別の時点では違う式になる ● 外部要因( ここでは dpi )を注入して関数を組み立てるような設計にしたい ○ dpi は更に window size に依存する 43 時間経過によって変動する式

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

ところで 45

Slide 46

Slide 46 text

● その通り ○ メソッドが1個しかないクラスに対する DI みたいなものだと思って ● むしろ自分は DI をクラスで部分適用するものと見ている ○ new Service(arg).call(hoge) はカリー化の OOP 的解釈 ○ 関数だと呼出が 2 段以上続くケースも作りやすい ( service(arg)(hoge)(...) ) 46 これって DI みたいなもの?

Slide 47

Slide 47 text

● Q. 高階関数はともかくさ〜、if 文が ↓ みたいになるのイケてなくない? ○ if ( isOwnedBy(user)(item) ) { ● A. まぁそうですね ( uncurry しなければ ) ● いま ECMAScript に提案中の pipeline 演算子がきたら ↓ みたいに書けるので辛抱してく ださい ○ if ( item |> isOwnedBy(user) ) { 47 余談: Pipeline Operator

Slide 48

Slide 48 text

● カリー化を用いるとこんなことができる ○ 読みやすい高階関数の building block をつくる ○ 関数で DI する ○ 時間とともに変化する関数をつくる 48 まとめ