なぜ初心者にHaskellのファンクターは怖いと言われるのか(翻訳)
英語の勉強のためにWhy say the design of functor in Haskell is terrible for NEWBIE | neutronestを翻訳してみました。
以下翻訳
ファンクターって何?
今日私が話したい"ファンクター(functor)"は圏論(category theory)の概念ではなく、Haskellの中心的機能と言語の特性です。実はHaskellのファンクターのデザインは数学のコンセプトを起源にしています。最初にファンクターの定義の詳細を見なおしてみましょう。
class Functor f where fmap :: (a -> b) -> f a -> f b
このコードで、(a -> b)
という型シグネチャはa
という型の値を適用するとb
という型の値が返ってくる関数を意味します。ここでf
は関手の構造(functorial structure)(気をつけて!このf
は(a->b)
のような関数ではありません)です。従って、f a
は引数a
をf
に与えたという意味ではありませんが、型a
のものをf
という構造に入れたことを表します。
f
が困惑させる
今のところHaskell初心者には全く問題ないでしょう。私達マヌケな人間(そう、もちろん私も含めて)はHaskellの新しい重要な機能を理解することに大喜びします。続けてインスタンス実装です。例えば、リストは
instance Functor [] where fmap f [] = [] fmap f (x:xs) = (f x) ++ (fmap f xs)
ええと、待って、待って。ここのf
はどういう意味?他の関手の構造?もちろん違います。なぜなら[]
はファンクターの定義と対応しているf
なのでf
は(a -> b)
であるという型情報を私達は知っているからです。
多分誰かがこの違いはそんなに重要ではないのでファンクターの実装の型シグネチャをチェックするときだけ気をつければよいと言うかもしれません。OK、それらを一緒に混ぜた私達Haskell初心者をとても混乱させる例を見せましょう。
Prelude> let rmp = const 'p' Prelude> :t rmp rmp :: b -> Char Prelude> let lms = [Just "123", Nothing, Just "abc"] Prelude> :t lms lms :: [Maybe [Char]]
何も問題ないね?いくよ。
Prelude > fmap rmp lms "ppp"
簡単です。Chatのリスト(訳注:Charのタイポ?)を作るためにlms
のそれぞれの要素を'p'
に置き換えただけです。従って"ppp"
になります。
もっと複雑なもの
Prelude> (fmap . fmap) rmp lms [Just 'p',Nothing,Just 'p'] Prelude> :t (fmap . fmap) rmp lms (fmap . fmap) rmp lms :: [Maybe Char] Prelude> :t (fmap. fmap) (fmap. fmap) :: (Functor f, Functor f1) => (a -> b) -> f (f1 a) -> f (f1 b)
ここでは何が起きた?なぜfmap
を加えるとMaybe
型が出てくるのでしょう?(fmap . fmap)
のf (f1 a)
は何を意味しているのでしょうか?入れ子になった二重の関数呼び出し?実はコンパイラはf
とf1
が両方ともファンクターであることを知っています。しかし多くの人、Haskell初心者はf (f1 a)
を2つの関数呼び出しと見做すかもしれません。まずf1 a
を計算し、その値を最終的な結果を得るためにf
に適用するというふうに。
実際、この例は最新のHaskellの本(この本は最新のGHCのたくさんの機能と変更をカバーしていてとても良いです)から来ています。正直に言うと私はマヌケなので自分でファンクターの定義をもう一度書くまでの数日間ここで行き詰まりました。f (f1 a)
は私を困惑させ、入れ子になった関数ではなく入れ子の構造を意味していると理解するまでに時間がかかりました。
ファンクター?関数?
私はファンクターと関数が圏論で高いレベルで抽象化されて同じものになっているかどうか知りません。みんなが圏論で武装した研究者ではないのです。もしHaskell研究者が「ファンクターはほにゃららな構造で〜」と2つは別物であると決めても、f
は別物であるとしない方が良いです。
ファンクターへの個人的な変更
私がこの例にしばらく詰まったあと、本の関連するセクションを読み直してもっと簡単に理解するためにファンクターを書き直しました。
class Structor s where smap :: (a -> b) -> s a -> s b instance Structor [] where smap f [] = [] smap f (x:xs) = (f x)++(fmap f xs)
例を再検査して型情報を見る。
Prelude> (smap . smap) rmp lms [Just 'p',Nothing,Just 'p'] Prelude> :t (smap . smap) rmp lms (smap . smap) rmp lms :: [Maybe Char] Prelude> :t (smap. smap) (smap. smap) :: (Functor s, Functor s1) => (a -> b) -> s (s1 a) -> s (s1 b)
Functor
をStructor
に、fmap
をsmap
に、f
をs
(sはstructureの意味)に変えた以外何もしていません。これでs
とf
に惑わされることはありません。その上、s (s1 a)
を見た時にすぐに入れ子の構造であると分かります。
まとめ
今回私が話した問題はちっぽけです(ただ名前を選んだだけ)。数学の概念とそれに関連するHaskellの機能を持ち込むことは時には不必要です。初心者が理解することの障害になります。熟練したHaskellプログラマははっきりと分かっているのでこの問題を放置するかもしれません。モナドでも同様です。知識はないけどやる気のある人に困難を作らないでください。
翻訳ここまで
途中で「二重の関数呼び出しじゃなくて二重の構造」って言ってるのに、直後で「その2つを別物と思わないほうが良い」って書いてあるような気がするんですがどういうことでしょう。