Applicative、pure、関数についての再考、帰ってきた Functor

引き続き Haskell
前回は↓
関数がファンクターである、とは - doitakaの日記


タネ本は『すごいHaskellたのしく学ぼう!』。

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!


ファンクターについて色々まとめてみた勢いで、アプリカティブファンクターについて考えようとしたものの、見ていくうちにやはり自分は分かったようで分かっていなかったのではないか、と悩んでしまったので、色々と考えなおす。

Applicative、pure

Applicative 型クラスの実装。

class Functor f => Applicative f where
    pure :: a -> f a
    (<*>) :: f (a -> b) -> f a -> f b

『すごい』の説明。

pure は任意の型の引数を受け取り、それをアプリカティブ値の中に入れて返します。

<*> は関数の入っているファンクター値と値の入っているファンクター値を引数に取って、1つ目のファンクターの中身である関数を2つ目のファンクターの中身に適用するのです。


説明と例を見れば分かったような気にはなるのだが…というところである。
引き続き、アプリカティブファンクターとしての関数において宣言がどうなるか、 f に r -> を当てはめるとこうなる。

    pure :: a -> (r -> a)
    (<*>) :: (r -> (a -> b)) -> (r -> a) -> (r -> b)

実装はこうなる。

instance Applicative ((->) r) where
    pure x = (\_ -> x)
    f <*> g = \x -> f x (g x)


このあたりでお手上げになってくる…。
とりえあず pure の説明を『すごい』から引くと、

ある値を、pure を使ってアプリカティブ値に包んだものは、元の値を生み出せないといけません。つまり、その値を再現できるような最小限のデフォルト文脈でなければなりません。そこで、関数のアプリカティブなインスタンスの実装における pure は、値を取って「引数を無視して常にその値を返す関数」を作るようになっています。

となっている。
「その値を再現できるような最小限のデフォルト文脈」…???
ここまで来て、「文脈」とは何なのかということについて、イメージを明確にできていなかったことが浮き彫りになってきた感がある…。


関数にとってのデフォルトの文脈とは何であるか。Functor では登場しなかったデフォルトの文脈という概念が、なぜ Applicative では登場する必要があったのか。 ……このあたりを再考してみたい。

関数についての再考

まず、関数における pure の動作を確認する。

pure x = (\_ -> x)

なんなんだこれは…というところだが。
『すごい』の例では、

ghci> (pure 3) "blah"
3

となっている。
この時、pure 3 は、型としては [Char] -> Int になっているはずである。


だが、定義から明らかなように、pure で作られた関数は、どのような値にも適用可能である。

Prelude Control.Applicative> (pure 3) 2
3
Prelude Control.Applicative> (pure 3) [1,2,3]
3
Prelude Control.Applicative> (pure 3) ("a", "b", "c")
3


この関数は、ほとんど定数である。
言い換えれば、「何らかの値に適用することで定数になる関数」となる。
そして、もっと言えば、その「定数」は、すなわち、pure の引数となっていた「元の値」である。
ならば、「何らかの値に適用することで元の値になる関数」である。
これは、「ある値を、pure を使ってアプリカティブ値に包んだものは、元の値を生み出せないといけません」という説明の通りとなる。
では、「つまり、その値を再現できるような最小限のデフォルト文脈でなければなりません」と言われている時の、「最小限のデフォルト文脈」とは何なのか。


……と、ここまで書いてきてだが、整理できているようで整理できていないような違和感がある。
「x に pure を適用することで、(pure x) という関数ができる」の時点では、x は目的語的である。
が、「(pure x) を y に適用する」では、(pure x) は動詞的な扱いになる。
そして、「 (pure x) y の結果、 x が得られる」で、また x が目的語に戻っている。
x という要素を中心にして見ようとしても、x の扱いがころころ変わってイメージしづらい。問題はどうも、「関数を値に適用する」という言い方の部分のようである。
つまり……、「関数を値に適用する」という言い方では、関数が動詞のように見えてしまい、「値が関数によって変化した」というニュアンスになる。これに従うと、(pure x) y のケースでは、「(pure x) を y に適用した結果、y が x に変化した」ということになってしまう。これはどうもおかしい。少なくとも、(pure x) という関数においては、このイメージは適切でないように見える。


ならばということで、「値を関数に与える、しかるのち、関数適用により値を生み出す」という言い方をしてみる。
これにより、「(pure x) を y に適用した結果 x が得られる」を、「y を (pure x) に与える、しかるのち、関数適用により x を生み出す」と言い換えることになる。
もっと言えば、「所与のものである y に対して、アクションとして (pure x) を適用する」でなく、「所与のものである (pure x) に対して、アクションとして y を与える」というイメージである。


この場合、(pure x) の性質はどうなるだろうか? 「将来に y が与えられた場合に x を生み出す」と言える。
そして、この動作は定数的であるため、 y が何であるかは全く生み出される値に影響しない。すなわち y という値は隠蔽されることになる。
(pure x) の性質が、 y の性質を隠蔽する……ということから、「箱」の働きが連想されないだろうか。文字通り、「函数」である。
図示すれば、以下となる。



そして、(pure x) の関数としての挙動が"純粋"であるのは、箱全体の性質が不変であるから、と見ることができるのではないだろうか。


Haskell においては、関数の型は、 a -> b と記述するので、関数が、動詞的な、固定されていないもの、というイメージがつきやすいのではないか。だが、関数により生み出される値の型は b である。ならば、関数を 「a -> というアクションを受けることができる、b 型の値」と読み替えることが可能ではないだろうか。
この点については、C言語の関数のような、「 int func( int x ) 」という記法の方がイメージしやすいかもしれない。
・ func( ) の全体の型は int である。
・ func( ) のカッコ内は空白で、ここに x で表される値を与えることができる。
という具合である。


上記のような見方をすることで、関数についてのイメージがいくらかすっきりしてきたような気がする。
では…改めて、関数にとっての「文脈」と言えるものは何なのか。
これはもう素直に、 r -> の部分、すなわち、関数に与えられる値、つまり、引数、と見てしまっていいのではないだろうか。
前回、ファンクターについてまとめた際は、

つまり、ファンクターとしての関数においては、維持される「文脈」は、「どの型に適用可能か」ということ。
で、関数は、関数合成できる、という性質を持つ故に、Functor とみなせる、ということであろう。

と書いたわけだが……内容的には似たようなものなんだけど見方が違う、という感じである。


ただ、この場合は、『すごい』の pure についての説明にある「その値を再現できるような最小限のデフォルト文脈」という部分が不明になってしまう。また再考することになるかもしれないが、一旦これは置いておく。


帰ってきた Functor、関数適用 と fmap の再考

関数について一度整理しなおしたところで、では、この見方で Functor を見直すとどうなるか。


Functor は、以下の性質を満たすもの、要するに fmap に対応するものである。

class  Functor f  where
    fmap        :: (a -> b) -> f a -> f b

fmap は、リストでの map のイメージがあるので、「fmap 関数 ファンクタ値」 という形が普通であるように見えてしまうが、これは、(a -> b) を中心に見てみれば、関数((a -> b))に、ファンクタ値(f a)を与えているだけ、ということにならないだろうか。


通常の関数適用は、f x として表されるが、これは $ を使うことで f $ x と書き直せる。
さらに、中置記法でなく、2つ引数を取る形に書き直すと、 $ f x となる。
改めて見れば、$ は、関数 f と x を取って、x に f を適用する関数である。
型を確認すれば以下となる。

Prelude Control.Applicative> :t ($)
($) :: (a -> b) -> a -> b


あれれー、これって何かに似てませんかねー。
ということで、 $ 、 fmap = <$> 、 <*> を並べてみるとこうなる。

($) :: (a -> b) -> a -> b
(<$>) :: Functor f => (a -> b) -> f a -> f b
(<*>) :: Applicative f => f (a -> b) -> f a -> f b

fmap を f `fmap` x = f <$> x という書き方にすれば、$ による通常の関数適用と同じ形であることが分かる。 <*> も同様。つまり、<$>、<*> は「文脈」が関わるケースでの関数適用の特殊系と見れる。


$ 、 <$> 、 <*> を同類として見て、「値に関数を適用する」でなく、「関数に値を与える」という見方をするとどうなるか?
$ は、関数に値を与える。<$> は、関数に文脈つきの値を与える。<*> は、文脈つきの関数に、文脈つきの値を与える。
となる。


ひとまず、 <*> は手強そうなので置いておいて、 $ と <$> について整理してみる。
それぞれを以下と言い直せる。(区別のため、ファンクターの型は F とする)
f $ x   : 関数適用により b 型を生み出す関数 f に a 型値 x を与えると、b 型の値が生み出される
f <$> x : 関数適用により b 型を生み出す関数 f に F a 型値 x を与えると、 F b 型の値が生み出される


???
これは、<$> によって関数 f の型が変化した、ということになる。
つまり、文脈つきの値である x が文脈を維持したまま変化した、のではなく、x の文脈が一度外されて、f に文脈が引き継がれた、という見方をすることになる。
図示すれば以下である。



……「文脈を引き継ぐ」とはどういうことか。


ついでに、<*> も同じ言い方をしてみると……
f <*> x : 関数適用により b 型を生み出す関数 f が F の文脈をもったものに F a 型値 x を与えると、 F b 型の値が生み出される


これはわからんわ……。

つづく。