Help us understand the problem. What is going on with this article?

HaskellのApplicativeについて[発展編]

HaskellのApplicativeについて[発展編]

前回に続いて、HaskellのApplicativeを見ていきましょう。Alternativeについては別の記事に書きます。

注意

  • 説明に間違いなどあれば指摘していただけると嬉しいです。
  • この記事で使われているGHCのバージョンは8.8.3です。

ZipList

前回のアプリカティブなリストでは、非決定性計算(答えになる可能性のある値が複数ある)という特性上、関数リストとリストを<*>でつなぐと全ての要素にそれぞれの関数が適用されるのでした。

Prelude> [(+10),(*3),(\x->x^2+4)] <*> [1,2,3,4]
[11,12,13,14,3,6,9,12,5,8,13,20]

では、関数リストとリストをそれぞれこう適用したいときはどうすればいいのでしょう。

  [f, g, h] <*> [a, b, c]
= [f a, g b, h c]

それぞれの関数が、同じ位置にある要素に適用されます。これを行うのがZipListです。

newtype ZipList a = ZipList { getZipList :: [a] }
                  deriving (Show, Eq, Ord, Read, Functor,
                            Foldable, Generic, Generic1)

ZipListApplicativeとしてのインスタンス定義は、こうなっています。

instance Applicative ZipList where
    pure x = ZipList (repeat x)
    liftA2 f (ZipList xs) (ZipList ys) = ZipList (zipWith f xs ys)

liftA2(<*>)で、2つのZipListコンストラクタに包まれたリストを挟み合わせることができます。pureの定義は無限リストになっていますが、こうなっているのはどんな長さのリストでも対応できるのと、短い方のリストに合わせて切り捨てられるから、という理由があります。また、Applicativeの定義から、ZipListはFunctorのインスタンスになっています。

使ってみましょう。

Prelude> getZipList $ ZipList [(+5),(*3),(^2)] <*> ZipList [1,2,3,4,5]
[6,6,9]

しっかり挟み込んで適用されています。挟んだ値に型コンストラクタを適用させることもできます。

Prelude> getZipList $ (,) <$> ZipList ["a","b","c"] <*> ZipList [1,2,3]
[("a",1),("b",2),("c",3)]

Control.Applicative

(<**>) :: Applicative f => f a -> f (a -> b) -> f b
liftA :: Applicative f => (a -> b) -> f a -> f b
liftA3 ::
  Applicative f => (a -> b -> c -> d) -> f a -> f b -> f c -> f d

上の3つがControl.ApplicativeにあるApplicative関連の関数です。liftAは1引数の関数を、liftA3は3引数の関数をそれぞれApplicativeに持ち上げて計算します。<**><*>の引数を入れ替えたバージョンです。

Prelude> liftA length (Just [1,2,3])
Just 3
Prelude> liftA3 foldl (pure (+)) (pure 1) (Just [1,2,3,4,5])
Just 16

応用例

もっと長いliftA_

ところで、liftA3はこう定義されています。

liftA3 :: Applicative f => (a -> b -> c -> d) -> f a -> f b -> f c -> f d
liftA3 f a b c = liftA2 f a b <*> c

関数がカリー化されていることを利用して、liftA2に3引数関数と2つ目の引数までを食わせて、それをさらに3番目のcにつなげています。これを応用して、liftA4を作ってみます。

liftA4 :: Applicative f => f (a -> b -> c -> d -> e) ->
                      f a -> f b -> f c -> f d -> f e
liftA4 f a b c d = liftA3 f a b c <*> d
-- f <$> a <*> b <*> c <*> d と同じ

型注釈が長ったらしくなりますが、要するにカリー化されたf (d -> e)を最後のアプリカティブ値dに適用させるだけです。さらに長いliftA_もこれと同じ仕組みで作れます。

do構文の置き換え

モナドを扱う関数を書いていると、doのあとにアクションを書き連ねていってしまいますよね。

f = do
  c <- a
  d <- b
  return (g c d)

これは、アプリカティブスタイルで書き換えられます。

f = g <$> a <*> b

ApplicativeとMonadのインスタンスを何の障壁もなく使い分けられるのは、Monadがこう定義されているからです。

class Applicative m => Monad (m :: * -> *) where
  (>>=) :: m a -> (a -> m b) -> m b
  (>>) :: m a -> m b -> m b
  return :: a -> m a
  {-# MINIMAL (>>=) #-}

MonadのインスタンスになるためにはApplicativeのインスタンスにもなっている必要があるため、Applicativeの関数をそのまま使えます。

例えば、こんな関数があるとしましょう。

pythagoras :: IO Double
pythagoras = do
    a <- read <$> getLine
    b <- read <$> getLine
    return $ sqrt $ (a^2+b^2)
Prelude> pythagoras
3
4
5.0

これを、アプリカティブ・スタイルで書き換えます。

pythagoras' :: IO Double
pythagoras' = (\x y -> sqrt $ read x ^2 + read y ^2) <$> getLine <*> getLine

一時的な変数が不要になって、関数適用をまとめることができます。例えば、パーサーを書くときなどに、関数部分をまとめてアプリカティブ・スタイルで書くと、不要な変数を書かずに済みます。

Monadとの違い

Monadは、できることの範囲がApplicativeに比べてかなり大きいです。大きな要因は、型の制限のレベルの違いです。Applicativeの最小定義として<*>があり、もちろんこれはMonadのインスタンスにも使えます。型注釈はこうなっています。

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

例えば、こんな関数を書いたとします。

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

この関数の中で、gaのApplicativeの殻の種類は同じでなければなりません。ここまでは大丈夫です。

これはどうでしょう。

g :: Applicative f => f (a -> b -> c) -> f b
g = h <$> a <*> b

値を型に置き換えると、よくわかります。

  h <$> a <*> b
= (f (a -> b -> c))
  <$> (f a)
  <*> (f b)
= (f (a -> b -> c))
  ((a -> b) -> f a -> f b) (f a)
  (f (a -> b) -> f a -> f b) (f b)

この中で、bの殻の種類は<*>の殻の種類に、a<*>の殻の種類は<$>の殻の種類に依存しています。そして、<$>の殻の種類はhと同じでなければなりません。ですから、habの殻の種類は全て同じである必要があります。また、<*>の第1引数は関数でなければなりません。ですから、関数の引数の数しかチェインできません。

ところが、Monadはそんなことお構いなしにいくらでもチェインさせられます。>>=のおかげです。

(>>=) :: Monad m => m a -> (a -> m b) -> m b

>>=の第1引数はm a、つまりモナドであって、関数ではありません。そして、>>=は「文脈付きの値」を「普通の値を取って文脈付きの値を返す関数」に適用させる関数ですから、この計算の返り値はモナド値です。これを>>=の左辺に置いて…という風に関数を任意の数だけくっつけられます。

さらに言うと、Applicativeは繰り返しや計算の失敗の伝播はできますが、前の計算の結果から次の処理を変える、といった分岐ができません。

Prelude> getLine >>= (\i -> if i == "" then putStrLn "give me some input" else putStrLn i)
3290
3290
Prelude> getLine >>= (\i -> if i == "" then putStrLn "give me some input" else putStrLn i)

give some input

何かしらのアプリカティブ値をとる関数の型が下のようであったら、あるいは可能かもしれません。1

<?> :: f a -> (f a -> f b) -> f b
<??> :: f a -> (a -> f b) -> f b
<???> :: f a -> f (a -> f b) -> f b
<<?>> :: f a -> (f a -> b) -> f b

<??>はそのまま>>=<?>$ですが、返り値を次の計算の入力にするのですから、受け取る値と返り値が文脈付きの値でなければならないのは明らかです。

おまけ:Pointedについて

実は、Functorの拡張でpureに似たpointedという関数を提供するPointedという型クラスがあるのですが、GHCの標準には含まれておらず、2020年現在のHackageを見てもdependeciesがbase (>=4.5 && <5)となっています。Applicativeとの大きな違いは、copointedという関数を使って文脈を引きはがせることです。

class Copointed p where
  copoint :: p a -> a

instance Copointed ((,) a) where
  copoint = snd

instance Copointed ((,,) a b) where
  copoint (_,_,a) = a

instance Copointed ((,,,) a b c) where
  copoint (_,_,_,a) = a

このように、Copointedから値を取り出せるようです。

参考文献


  1. どんなパターンであっても、f a -> _ (_ -> _ _ b) -> f bのような形になるのは確実でしょう。第2引数の関数に適用させた結果もしくはそれを包んだ値を返り値にする必要があるからです。 

Izawa_
Haskell使いです。記事はあまりモナモナしてません。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした