読者です 読者をやめる 読者になる 読者になる

Functor における map の引数の順序を考えてたらいっこストンと腑に落ちた話

別に知見は書いてないですが、なるほどなーと思ったという感想を書いたエントリです。

ScalazとHaskellのFunctorの提供するmap(fmap)は、引数の順番が異なります。

  • Scalaz の Functor

    • def map[A, B](r: F[A])(f: A => B): F[B]
    • F[A] なFunctor値が最初の引数で、A => B な関数が次の引数で、F[B]なファンクター値が返り値
  • Haskell の Functor

    • fmap :: f => (a -> b) -> f a -> f b
    • (a -> b) な関数が最初の引数で、f a なファンクター値が次の引数で、f b なファンクター値が返り値。

つまり第一引数と第二引数が逆。

ふつうに考えると Scalaz のやつが直感的に思えます。C言語とかでも、ある構造体を操作するための関数って大体第一引数にその構造体を渡して、他のパラメータをその後に渡すし、Perlとかだって $nyan->do_something したら $nyan が第一引数に渡ってくるし。

なんでなんでそうなっているのか調べたわけではないんだけど、関数のリフティングするときにはHaskellみたいになってたほうが便利だよなーと思い当たって「ふーむなるほど」となりました。

要するに、Haskell スタイルだと f :: (a -> b) な関数を g :: (f a -> f b) に変換したいというときに、なにも特別なことをしなくても fmap f とすれば部分適応されるので自動的にそっから g :: (f a -> f b)を得ることができてうれしい。

一方 Scalaz はデータに注目した場合は直感的ではあるのだけれど、Liftingのためのメソッド lift の実装は def lift[A, B](f: A => B): F[A] => F[B] = map(_)(f) となっていて、引数の順番が違うせいで一枚噛ませる必要がある。

これ、言語の特徴を捉えていて面白いな、と思いました。つまり、Scalaオブジェクト指向的な考えで、第一の関心が「オブジェクト」の側にある。だから「最初にFunctor値を受け取って、それに対して関数を適用しますよ」という引数の順序のほうが自然なんだけど、liftみたいな"関数に主眼が置かれた操作"をやるときにScalazスタイルだと「引数の順番が逆だったらな〜」ってなる。

逆に、Haskellスタイルは第一の関心が「関数の側」にある。だから「関数をリフティングしたい」みたいなときは自然にかけるんだけど、データ(この例で言えば Functor値)のほうに注目していると、この引数の順序ってなんとなく不自然に思える。

で、思ったんですけど、これ、fmapに限らず、filterとかでもそうですね。Haskellはだいたい関数を第一引数に取るようになっていて、部分適応してやることによって新しい関数を作り出しやすいようになっている。

なるほどな〜〜〜という感じでした。

(さらに気づいたことを追記)

これ、型推論上の都合もありそう。型推論が左から右に流れるから、先にF[A]が来てないと、実際のFunctor値の型から A => B の A を推論してくれないという都合があるかもしれない。