LoginSignup
18
8

More than 5 years have passed since last update.

Applicative スタイル `f <$> m1 <*> m2` を読み解く

Last updated at Posted at 2017-07-16

Applicative スタイル f <$> m1 <*> m2 を読み解く

Applicative スタイルとは

foo = do
  a <- m1
  b <- m2
  return $ f a b

という計算に対し、 do と return を消して

foo = f <$> m1
        <*> m2

という風に書けるよという話です。

問題はこの f <$> m1 <*> m2 では実際に何が起きているのか、なぜ同じ計算結果になるのか、についてです。

<$><*>fmapapply

<$><*> という見慣れた、しかし初心者には分からん殺しの演算子ですが、両方とも infixl 4 なので、普通の左から順に計算できる2項演算です。

<$>fmap

まずこの <$> についてですが、 fmap という Functor 型クラスに関する関数です。様々な言語でリストに対する map としてよく使われているアレです。

fmap :: Functor f => (a -> b) -> f a -> f b

この型は一見すると 2項演算 のように見えますが、 Haskell では全ての関数はカリー化されているので

fmap :: Functor f => (a -> b) -> (f a -> f b)

という結合順序で読むと、関数 (a -> b) を引数に取り関数 f a -> f b を返す1項演算子と見ることができます。まさしく Functor ですね。

<*>apply

次に <*> についてですが、これは Applicative 型クラスに関する関数 apply です。
([\x->x+1, \x->x+2, \x->x+3] <*> [1, 2, 3]) == [2,3,4, 3,4,5, 4,5,6] というように使えます。

apply :: Applicative f => f (a -> b) -> f a -> f b

例によって1項演算子として見ると

apply :: Applicative f => f (a -> b) -> (f a -> f b)

と読めます。

Applicative スタイル を読み解く

では改めて

foo = f <$> m1 <*> m2

を式変形していきましょう。

foo = f <$> m1 <*> m2
foo = (f <$> m1) <*> m2
foo = (f `fmap` m1) <*> m2
foo = ((fmap f) m1) <*> m2
foo = ((fmap f) m1) `apply` m2
foo = (apply ((fmap f) m1)) m2
foo = ((apply ((fmap f) m1)) m2)

((apply ((fmap f) m1)) m2) という形になり、これで計算の順序は一点の曇りなく書くことができました。
全てのカッコでの型を書くと。

--      + apply :: Applicative f => f (a -> b) -> (f a -> f b)
--      |       + fmap :: Functor f => (a -> b) -> (f a -> f b)
--      |       |    + a -> b -> c
--      |       |    |  + f a
--      |       |    |  |    + f b
--      |       |    |  |    |
foo = ((apply ((fmap f) m1)) m2)
--    ||      ||
--    ||      |+ f a -> f (b -> c)
--    ||      + f (b -> c)
--    |+ f b -> f c
--    + f c

となっています。というわけで foo の型は Applicative f => f c でした

do スタイルとの比較

なぜこの Applicative スタイル が do スタイルの結果と一致するのでしょうか。

この計算

foo = do
  a <- m1
  b <- m2
  return (f a b)

でやりたかったのは、ある Monad m1m2 の中身を取り出して関数 f に適用し、その結果 f a b を何らかの Monad で包むことです。

ところで、 Monad とは Applicative な Functor (Monad ⊆ Applicative ⊆ Functor) のうち2項演算子 bind (>>=) という性質もつもののことです。この >>= があることによって計算の結果を見て条件分岐し,
結果を変えることができます。

Monad 的な if 3項演算子 miffy を考えてみます。

miffy :: Monad m => m Bool -> m a -> m a -> m a
miffy mb mt me = do
  b <- mb
  if b then mt else me
{-以下のような意味です
  if b
    then do
      ret <- mt
      return ret
    else do
      ret <- me
      return ret
-}

mb の実行結果によって2つの mt と me からどちらか1つを実行し、結果を返す関数です。

Applicative Functor についても考えてみます。

iffy :: Applicative f => f Bool -> f a -> f a -> f a
iffy fb ft fe = (pure cond) <*> fb <*> ft <*> fe
  where
    cond b t e = if b then t else e

しかしこの定義には問題があります。

iffy (pure True) (pure t) Nothing == Nothing

条件は True なので結果は t になるべきですが Nothing を返してしまいます。なぜなら、

instance  Functor Maybe  where
    fmap _ Nothing       = Nothing
    fmap f (Just a)      = Just (f a)

instance Applicative Maybe where
    pure = Just

    Just f  <*> m       = fmap f m
    Nothing <*> _m      = Nothing

の通り、MaybeのFunctor型クラスのインスタンスはfmapの第二引数にNothingがくるとNothingを返すように定義されているからです。

Monad 版では問題ありません。

miffy (return True) (return t) Nothing == (return t)

このことから、Monad は Applicative Functor のうち条件分岐が可能な性質を持つものと見ることができます。

件の計算

foo = do
  a <- m1
  b <- m2
  return (f a b)

では m1の実行結果を参照して次に実行するモナドをm2とは別のモナドに分岐するような処理はありませんでした。
そのため、計算 foo はモナドのうち Applicative の性質のみで記述することができたのです。

逆に言えば、条件分岐する必要がなければ IO モナドであっても Applicative スタイル で逐次実行を書くことができます。

main :: IO ()
main = id <$> putStrLn "hello world"

main :: IO ()
main = (const id) <$> putStrLn "hello"
                  <*> putStrLn "world"

main :: IO ()
main = (const (const id)) <$> putStrLn "hello"
                          <*> putStrLn "real"
                          <*> putStrLn "world"

面白いですね。

参考

18
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
8