Haskellerへの道 #14 - Applicative

Last Edited: 6/27/2024

このブログ記事では、Haskellの重要な型クラス、Applicativeを紹介します。

Haskell & Applicative

Applicative ファンクター

Applicative ファンクターは、ファンクターのアップグレード版です。ファンクターについて自信がない場合は、前回の記事、Road to Haskeller #13 - Functorsを確認してください。 fmap を、たとえば2つのパラメータを必要とする関数に対して使用することを考えてみましょう。例えば、fmap (*) (Just 3) です。どうなるでしょうか? これは、Just 内に部分関数を作成します。例えば、Just (*3) のようなものです。この部分関数は、さらに fmap に渡すことができます。

ghci> :t fmap (*) (Just 3)
fmap (*) (Just 3) :: Num a => Maybe (a -> a)
ghci> let b = fmap (*) (Just 3)
ghci> fmap (\f -> f 9) b
Just 27

しかし、fmap (\f -> f (Just 9)) b を行うと、これは機能しません。なぜなら、fmap はファンクターの内部の型と同じ型の関数にのみ適用されるからです。 ファンクターの上の関数を他のファンクターに適用できるようにするために、Applicative 型クラスが Control.Applicative モジュールで定義されています。 このクラスは以下のように定義されています。

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

最初の部分、(Functor f) => Applicative は、fApplicative になるためにまずファンクターである必要があることを意味します。 したがって、Applicative 型クラスのすべての型コンストラクタはファンクターでもあり、fmap を使用できます。pure は簡単で、入力値を持つ Applicative ファンクターを作成します。 <*> は興味深い関数で、内部に関数を持つファンクターを取り、その関数を別のファンクターに適用します。 これは、<*> を使用して次のようなことができることを意味します。

ghci> (<*>) (Just (*3)) (Just 9)
Just 27
ghci> Just (*3) <*> Just 9
Just 27

MaybeApplicative の有効なインスタンスであることを見てみましょう。

instance Applicative Maybe where
    pure = Just
    Nothing <*> _ = Nothing
    (Just f) <*> something = fmap f something

pure が部分関数適用でJustの値を作るよう定義されています。<*>では、Nothing が提供された場合は操作を回避し、 Just から関数を抽出し、それを fmap に渡すように定義されています。これらの関数を使用すると、次のような興味深いことが実行できます。

ghci> pure (+) <*> Just 3 <*> Just 5
Just 8
ghci> Just (*) <*> Just 3 <*> Just 5
Just 15

Applicative のセットアップを使用することで、複数のパラメータを持つ関数を applicative ファンクターの値に使用して部分関数や出力値を生成できます。 pure の代わりに、<$>を使用して fmap を実行し、次の操作のためのファンクターを作成することもできます。

ghci> (+) <$> Just 3 <*> Just 5
Just 8

上記は fmap (+) (Just 3) を実行して Just (+ 3) を作成し、<*>を使用して Maybe 内の操作を実行しています。

リスト

リストも Applicative 型クラスの有効なインスタンスであり、次のように実装されています。

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

上記から、リスト内包表記が <*> の定義に利用されていることがわかります。リスト内包表記に自信がない場合は、 Road to Haskeller #3 - Lists & Tuplesを確認してください。Maybe と同様に、次のようなことができます。

ghci> [(*0),(+100),(^2)] <*> [1,2,3]
[0,0,0,101,102,103,1,4,9]

上記からわかるように、<*> は左側のリストを反復処理し、右側のリストの各要素に対して操作を実行します。 以下の例を見ると、操作の順序をよりよく理解できます。

[(+),(*)] <*> [1,2] <*> [3,4]
--- 上記は次のように変換されます:
--- [(+1), (+2), (*1), (*2)] <*> [3, 4]
--- [(3+1), (4+1), (3+2), (4+2), (3*1), (4*1), (3*2), (4*2)]
--- 結果: [4, 5, 5, 6, 3, 4, 6, 8]

<$> を使用して、次のようなこともできます。

filter (>50) $ (*) <$> [2,5,10] <*> [8,10,11]
--- 上記は次のように変換されます:
--- filter (>50) $ [(*2), (*5), (*10)] <*> [8,10,11]
--- filter (>50) [(8*2), (10*2), (11*2), (8*5), (10*5), (11*5), (8*10), (10*10), (11*10)]
--- filter (>50) [16, 20, 22, 40, 50, 55, 80, 100, 110]
--- 結果: [55, 80, 100, 110]

IO

IOApplicative の有効なインスタンスであり、次のようにインスタンスが実装されています。

instance Applicative IO where
    pure = return
    a <*> b = do
        f <- a
        x <- b
        return (f x)

pure 関数は単に return です。return は何も行わない IO アクションを生成します。 <*> 関数は <- 構文を使用して IO アクションの出力を抽出し、内部操作を実行します。 次に、結果を return でラップして IO アクションに戻します。これにより、次のようなリファクタリングが可能になります。

--- 書き換え前
myAction :: IO String
myAction = do
    a <- getLine
    b <- getLine
    return $ a ++ b
 
--- 書き換え後
myAction = (++) <$> getLine <*> getLine

クイズ

この記事では、学習した内容を確認するためのクイズを設けます。記事のメイン部分を読んだ後に、ぜひ自分で問題を解いてみることを強くお勧めします。各問題をクリックすると答えが表示されます。

リソース