akfm.dev

lodashのflowやchainを数学的に分析する

July 13th, 2019

Introduction

最近圏論を学びはじめました。 関数型を学ぶ上で、言語->数学の順で学ぶことへの疑問が自分の中で溜まってしまったのと、久しぶりに数学勉強してみたくてしょうがなくなったのでやってみようかなと。 まぁまだまだ学び途中なんですが、備忘録も兼ねて今回は、lodashのchainを圏論的に見るとどうなのかを解説してみようかと思います。

lodashとは

lodashは関数型javascriptをサポートするユーティリティライブラリです。 多くのフレームワークやライブラリの内部実装に使われてたりもする、結構有名なライブラリです。 jQueryとかと違って、ここ数年はlodashやRxjsとかのプログラミングパラダイムをサポートするユーティリティ的なライブラリが出て結構盛り上がってる(と僕は思ってるので)、知らなかった人はぜひ試してみてください!

他の関数型サポートのライブラリ

ちなみに話ついでに。関数型系のライブラリだと

  • RamdaJS
  • immutableJS

とかが昨今だと関数型系のライブラリだと有名ですかね? Ramdaは僕はちゃんと使ったことがないんですが、ドキュメントとかみてると結構lodashと同じような関数が揃ってるイメージです。

あとはやっぱりReactやReduxなんかが関数型の影響を強く受けてたりするのと、最近よく聞くようになってきたElmが関数型言語なので、その辺も要チェックですね。 Elmネタはいずれ記事で書きたいと思います。

lodash/chainとlodash/fp/flow

lodashにはlodashとlodash/fpというものがあり、lodash/fpの方がより関数型スタイルを強めたもの(詳細は省きますが、デフォルトでカリー化されている)になります。 おそらくですが、lodash/fpの方が後発だと思います。 今回はこのlodashとlodash/fpにあるパイプライン的演算を可能にするlodash/chainとlodash/fp/flowでどのようなデータ構造が生成されているか、そしてlodash/chainからlodash/fp/flowに変化したことによって圏論的にはどのような構造変化があったのかみてみようと思います。

chain

lodashを直接importするとバイト数が大きくなるのであまりよろしくないんですが、今回は最適化が目的ではないので一旦わかりやすさ重視でいきたいと思います。 chainでは値を受け取って加工してから、最後に_.value()で値を取り出せます。

import _ from 'lodash'

const result = _.chain([1, 2, 3, 4, 5])
  .map(x => x * 2)
  .filter(x => x <= 6)
  .value()

console.log(result)

細かいメソッドの説明は省きますが、この例ではchainで配列を受け取ってからlodashのプロトタイプで定義されているメソッド(map, filter, value)を実行しています。 

Comonadというアプローチ

このchainで生成されたデータ構造は圏論的?に言うと コモナド と言われる構造です。 コモナドはコモナド則という規則を満たす構造で、圏論的にはいわゆる モナド と双対する存在です。

chainで生成されたデータ構造は_.valueを実行するまで隠蔽されています。 コモナドも同様に計算途中では値は隠蔽されており、必要になった場面で値の取り出し操作が発生します。

Haskellにもコモナドは定義されているので、Haskellの型定義を参考にみてみましょう。 ※急にHaskellが出てきましたがご容赦ください。

class Functor w => Comonad w where
  extract :: w a -> a
  extend :: (w b -> a) -> w b -> w a
  duplicate :: w a -> w (w a)

  duplicate = extend id
  extend f = fmap f . duplicate

extractのw a -> a は、ラッピングされたaという型(w a)からaという値を取り出すことを、 extendのw b -> a はラッピングされた引数(w a)からbという型の結果を返す関数を引数に取ることを表しています。

chainで生成された構造も、プロトタイプで繋がれたメソッドにb -> a となるような関数を渡して新たな関数 w b ->a を生成し、ラッピングされた値(w b)へ適用し、戻り値(w a)を得ているとみなせますね。 最後のvalueメソッドでw a -> aであるextract同様の振る舞いをしているのでまさにコモナドです。

flow

次にlodash/fp/flowの挙動をみてみましょう。 ※今度はpartial importしてますが、主題じゃないので詳細は省きますが単なる最適化です。

import map from 'lodash/fp/map'
import filter from 'lodash/fp/filter'
import flow from 'lodash/fp/flow'

const result = flow(
  map(x => x * 2),
  filter(x => x <= 6)
)([1, 2, 3, 4, 5])

console.log(result)

mapやfilterに関数を渡した際の戻り値がa -> b型の関数に変わってたり、値の取り出しに必要だったvalueメソッドがなくなりましたね。 先ほど説明したコモナドで定義されてた型とはだいぶ異なるので、これだとコモナドではなさそうですね。

これをTypescriptの型でみてみるとこんな感じになります。

<R1, R2>(f1: () => R1, f2: (a: R1) => R2): () => R2

関数を受け取って関数を返す、ほんとうにただの関数ですね。 構造的なラッピングとかはとくにないので、これはコモナドでもモナドでもなく、単純な関数になります。

lodash/fpではコモナドなどのアプローチよりも関数の組み合わせへ方向転換することで従来の使い勝手に近い状態でchainのpartial importやカリー化を行なったということになります。

まとめ

chainとflowは単純な互換的メソッドのように見えますが、実は内部アプローチ自体大きく変更されていることがわかりました。 この変更を見たときは「ああ、シンプルになったしよかったな」くらいにしか思ってなかったんですが、こうしてみると結構おおきな変更に見えますね。

今回はlodashのchainがコモナド生成であることに絞りましたが、今後また圏論ネタでElmとかもかけていけたらと思います。