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

HaskellのApplicativeについて[基本編]

Functorの回では、Functorが包まれた値に関数を適用するためのツールだということを説明しました。この記事では、その上位互換であるApplicative(別名Applicative Functor)、そしてアプリカティブ・スタイルと呼ばれる記法について解説していきます。

※思ったよりも長くなってしまったので、Control.ApplicativeやZipListについては次の記事で解説します。

注意

  • Functorの回と同様、筆者の知識不足により数学のお話はできません。1
  • 使用したGHCのバージョンは8.8.3です。

Applicativeの基本

ApplicativeもFunctorと同じように包まれた値を扱いますが、Functorとは少し毛色が違います。Functorはこういうものでしたね。

fmap.png

包まれた値と関数を受け取り、関数を適用して、再び包んで返す。Applicativeはこうです。

applicative.png

関数も箱に包まれているのがわかりますか?Applicativeは箱に包まれた値と関数を扱う型クラスです。

さて、以下がApplicativeの定義です。

class Functor f => Applicative (f :: * -> *) where
  pure :: a -> f a
  (<*>) :: f (a -> b) -> f a -> f b
  liftA2 :: (a -> b -> c) -> f a -> f b -> f c
  (*>) :: f a -> f b -> f b
  (<*) :: f a -> f b -> f a
  {-# MINIMAL pure, ((<*>) | liftA2) #-}

Applicativeのインスタンスにするためにはpure<*>を実装すればよいことがわかります。標準でインスタンスになっているのは、次の型たちです。

Either e, ZipList, WrappedArrow, [], Maybe, IO, ((->) a), ((,) a)

pure

pure :: a -> f a

pureは、包まれていない値を受け取り、文脈にあった値を返します。

Prelude> :t pure 3
pure 3 :: (Applicative f, Num a) => f a
Prelude> pure 3 :: Maybe Int
Just 3
Prelude> pure 20 :: [Int]
[20]
Prelude> pure "haskell" :: IO String
"haskell"

一番上の例ではpure 3の型はMaybe Intと指定されていますから、Applicative Maybeに従います。

instance Applicative Maybe where
    pure = Just
    ...

Justは値aを一つ受け取ってMaybe aを返す単なる値コンストラクタなので、この定義は自然ですね。pure文脈におけるもっとも小さい形の値を生み出す関数なのです。

<*>

infixl 4 <*>
(<*>) :: f (a -> b) -> f a -> f b

<*>は中置演算子で、関数をファンクタ値に適用する<$>の兄貴分です。<$>はこんな感じでしたね。

infixl 4 <$>
(<$>) :: (a -> b) -> f a -> f b

挙動はこんな感じ。

Prelude> (*3) <$> Just 20
Just 60
Prelude> (++"!") <$> ["haskell","lambda","functor"]
["haskell!","lambda!","functor!"]

<$>包まれた関数を扱うことはできませんが、<*>ならできます。

Prelude> Just negate <*> Just 49
Just (-49)
Prelude> Just (++"!") <*> Just "applicative"
Just "applicative!"

関数のリストを<*>を使ってリストに適用すると、興味深いことが起こります。

Prelude> [(^2),(*20),negate] <*> [1,2,3,4]
[1,4,9,16,20,40,60,80,-1,-2,-3,-4]

リストの関数それぞれがリストの要素それぞれに適用されたリストが返ってきました。リストは非決定性計算、すなわちいくつかの可能性を表現しているので、関数のリストももちろん非決定的なものです。ですから、非決定的なリストを非決定的なリストに合わせたら、さらに非決定的なリストが返ってきます。

この<*><$>を使って関数と包まれた値を繋げて書いていくことをアプリカティブ・スタイルと呼びます。

liftA2

liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c

liftA2は2引数関数に包まれた値を適用します。

Prelude> liftA2 (+) (Just 10) (Just 39)
Just 49
Prelude> liftA2 (+) [1..5] [1..7]
[2,3,4,5,6,7,8,3,4,5,6,7,8,9,4,5,6,7,8,9,10,5,6,7,8,9,10,11,6,7,8,9,10,11,12]

<*, *>

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

<*はこう実装されています。

infixl 4 *>
(*>) :: f a -> f b -> f b
a1 *> a2 = (id <$ a1) <*> a2

<$は左辺のファンクタ値のファンクタを右辺の値にかぶせる演算子でしたね。*>は左辺のアプリカティブをidに被せて、それをa2に適用します。idなので、そのままa2が返ってきます。<*はその逆です。

遊んでみる

Prelude>  (++) <$> Just [1,2,3,4,5] <*> Just [6,7,8,9,10]
Just [1,2,3,4,5,6,7,8,9,10]
Prelude> :t (++) <$> Just [1,2,3,4,5]
(++) <$> Just [1,2,3,4,5] :: Num a => Maybe ([a] -> [a])

この例では、引数を2つとる++をファンクタ値でもあるJust [1,2,3,4,5]に適用してJust ([1,2,3,4,5] ++)を作り、それをさらに<*>でアプリカティブ値としてのJust [6,7,8,9,10]に適用しています。型注釈を見れば、(++) <$> Just [1,2,3,4,5]包まれた関数だから適用できるとわかりますね。

もちろん、もっと引数がたくさんある関数でも使えます。

Prelude> foldl <$> [(+),(*)] <*> [1,2] <*> [[1,2,3],[4,5,6]]
[7,16,8,17,6,120,12,240]

最初に、foldl[(+),(*)]に適用されて[foldl (+), foldl (*)]が作られます。非決定的な値の場合はパターンを網羅する必要があるのですから、合計で2*2*2=8個の値が入ったリストが返ってきます。先程の[foldl (+), foldl (*)]がそれぞれ[1,2]をアキュムレータにとり、[foldl (+) 1, foldl (*) 1, foldl (+) 2, foldl (*) 2]が生成されます。これがさらにそれぞれ[1,2,3][4,5,6]を畳み込んで最終的にリストが返ってきます。

アプリカティブ・スタイルは引数がどんなに多くても<*>で引数を繋げて使うことができます。ただし、全てが同じ文脈である必要があります。(これについては後々説明します。)

アプリカティブ則

Applicativeにはアプリカティブ則というきまりがあります。これらはインスタンスをApplicativeとして使用できることを保証するもので、標準の実装は全てこれらを満たしています。

1. pure id <*> v = v
2. pure (.) <*> u <*> v <*> w = u <*> (v <*> w)
3. pure f <*> pure x = pure (f x)
4. u <*> pure y = pure ($ y) <*> u

一つひとつ見ていきましょう。

Identity (対称性)

pure id <*> v = v

pureが文脈に合う値を返すこと、<*>が正しくidを適用すること(余計なことをしないこと)を保証します。fは文脈そのものです。

  pure id <*> v
= f id <*> f v'
= f (id v')
= f v'
= v

試してみましょう。

Prelude> pure id <*> Just 39
Just 39
Prelude> pure id <*> [1,2,3]
[1,2,3]

Composition (合成)

pure (.) <*> u <*> v <*> w = u <*> (v <*> w)

uvwの文脈の部分を明示すると、値と文脈を切り離して考えられるので理解の助けになります。

  pure (.) <*> u <*> v <*> w
= f (.) <*> f u' <*> f v' <*> f w'
= f (u' . v') <*> f w'
= f (u' . v' (w'))
= f (u' (v' w'))
= f u' <*> (f (v' w'))
= f u' <*> (f v' <*> f w')
= u <*> (v <*> w)

例を示します。

Prelude> pure (.) <*> Just tail <*> Just reverse <*> Just [1..5]
Just [4,3,2,1]

最初にreverseが適用されて逆順になったリストのtailが返ってきています。

Homomorphism (同型性)

pure f <*> pure x = pure (f x)

pure fpure xはどちらも同じ文脈の値で、右辺はそれらが適用されている形です。<*>の動作を簡潔に示したものともいえるでしょう。

  pure f <*> pure x
= pure (f x)
Prelude> pure (+3) <*> pure 3
6
Prelude> pure 6
6

Interchange (可換性)

u <*> pure y = pure ($ y) <*> u

<*>の両辺を入れ替えた際の動作についての性質です。文脈と切り離して証明します。

  u <*> pure y
= f u' <*> pure y
= f (u' y)
= f (u' y)
= f (u' $ y)
= pure ($ y) <*> f u'
= pure ($ y) <*> u

入れ替えてみましょう。

Prelude> Just (*2) <*> pure 7
Just 14
Prelude> pure ($ 7) <*> Just (*2)
Just 14

標準ライブラリのApplicativeたち

Maybe

instance Applicative Maybe where
    pure = Just
    Just f  <*> m       = fmap f m
    Nothing <*> _m      = Nothing
    liftA2 f (Just x) (Just y) = Just (f x y)
    liftA2 _ _ _ = Nothing
    Just _m1 *> m2      = m2
    Nothing  *> _m2     = Nothing

pureは値をMaybeに包んで返すJustとして定義されています。<*>の左辺がNothingなら結果はNothing、そうでなければ適用して包んで返します。基本的に、アプリカティブ値がNothingなら結果もNothingになります。

Either e

instance Applicative (Either e) where
    pure          = Right
    Left  e <*> _ = Left e
    Right f <*> r = fmap f r

Left(計算の失敗を意味する)なら値はスルーされ、Rightなら関数が適用されます。

[]

instance Applicative [] where
    pure x    = [x]
    fs <*> xs = [f x | f <- fs, x <- xs]
    liftA2 f xs ys = [f x y | x <- xs, y <- ys]
    xs *> ys  = [y | _ <- xs, y <- ys]

リストはあり得る結果の集まりなので、関数も組み合わせ論的にすべての要素にそれぞれ関数が適用されます。ですから、<*>を用いて適用したとき、要素数は関数のリストの要素数×引数のアプリカティブ値の要素数になります。空リストならスルーされます。

(,) a

instance Monoid a => Applicative ((,) a) where
    pure x = (mempty, x)
    (u, f) <*> (v, x) = (u <> v, f x)
    liftA2 f (u, x) (v, y) = (u <> v, f x y)

purea -> f aという型の関数なので、pure x(?, x)になります。この時、?の値を勝手には決められません。<*><$>で操作しても問題がないようにしなければなりません。そこで、Monoid a =>という型制約を導入して単位元を?に持ってくることにしました。これによって、<*>でも問題なく使えます。<>はモノイド同士を結合させるので、引数のタプルの第1要素には影響を及ぼしません。

Prelude> pure (++"a") <*> ("a","b")
("a","ba")

IO

instance Applicative IO where
    pure  = returnIO
    (*>)  = thenIO
    (<*>) = ap
    liftA2 = liftM2

IOアプリカティブについては言語拡張やIOモナドが入ってきてややこしくなるので別の記事で説明します。

((->) r)

instance Applicative ((->) r) where
    pure = const
    (<*>) f g x = f x (g x)
    liftA2 q f g x = q (f x) (g x)

関数もアプリカティブ値です。各関数の型注釈のf((->) r)で置き換えてみましょう。

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

実際に試してみましょう。

Prelude> :t pure id
pure id :: Applicative f => f (a -> a)
Prelude> :t const id
const id :: b -> a -> a
Prelude> pure id 2 3
3
Prelude> const id 2 3
3

constは2つ引数をとり、第1引数を返す関数です。const idの挙動は次のようになると予想できます。

  const id a b
= (const id a) b
= id b
= b

abは任意の値です。まずconstidaに適用されてidが返ります。そしてそれがbに適用され、bが返ります。関数がカリー化されていることを考えると、purea -> (r -> a)a -> r -> aとも読めます。つまり第1引数のみを返す関数、つまりconstです。

関数を返す関数(アプリカティブな関数)を関数(関数としてのアプリカティブな値)に適用すると、関数が返ります。

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

GHCiで試しながら見ていきましょう。

Prelude> ((*) <*> (+2)) 3
15
Prelude> :t ((*) <*> (+2))
((*) <*> (+2)) :: Num a => a -> a
Prelude> :t (<*> (*3))
(<*> (*3)) :: Num a => (a -> a -> b) -> a -> b

(r -> (a -> b))は関数のカリー化を考慮すると2引数関数でも使えることがわかります。(*) 3 ((+2) 3)3 * (3 + 2)15になります。

まとめ

  • Applicativeは包まれた値と包まれた関数を扱うための型クラス
  • Applicativeのインスタンスにはルールがある

感想

アプリカティブ則、インスタンスの解説が予想よりも長くなったので、Control.Applicativeやモナドとの違い、応用は別の記事に書きます。Applicativeも奥が深いですね。

参考文献


  1. Functorが関手だというのはわかったのですが、Applicativeは圏論にはあるのでしょうか…圏論もいつか勉強したいですね。 

Izawa_
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
ユーザーは見つかりませんでした