関数型プログラミング言語を勉強していると、Monadだけでなく、FunctorとかApplicativeなど、オブジェクト指向に慣れ親しんだプログラマには聞きなれない言葉が出てきます。
今回は自分の勉強も兼ねて、Functorの使い所の代表的なパターンを見てみます。
※筆者は圏論の専門家ではないので、あくまでアプリケーションを作るプログラマ視点で、Functorの概要を整理しています。
Functorのおさらい
Functorのおさらいをするにあたり、Haskellを説明に使用します。
HaskellでのFunctorのソースコードを抜粋します。
以下、https://hackage.haskell.org/package/base-4.12.0.0/docs/src/GHC.Base.html#Functorより抜粋。
class Functor f where
fmap :: (a -> b) -> f a -> f b
Functorは「fmap」という関数を提供します。fmapは「引数として、型aを取り型bを返す関数と、f aを取り、f bを返す」関数です。
※上記の「f a」と「f b」の「f」はFunctorを表す。
※上記の型「a」および型「b」は任意の型を表す。
※あるインスタンスがFunctorであるためには、上記fmapの実装がFunctor則を満たす必要があります。詳しくは、以下のURLなどを参照。
https://hackage.haskell.org/package/base-4.12.0.0/docs/Data-Functor.html
Functorのfmap関数の使いどころ
上記のFunctorの説明に対して、見方を変えると、
「型aを取り型bを返す関数fに対してfmapを適用することで、その関数fをFunctorのコンテキストに持ち上げることができる」と読むこともできます。
文章だけだと分かりにくいので、以下に例を示します。
Prelude> let f = (+1) -- 関数 (+1)をfに束縛
Prelude> :t f -- fの型を表示
f :: Num a => a -> a -- fはNumクラスのインスタンスである型aを引数に取り、型aを返す関数
Prelude> let g = fmap f -- fに対して、fmapを適用する実装をgとして束縛
Prelude> :t g -- gの型を表示
g :: (Functor f, Num a) => f a -> f a --gは 「型(f a) を引数に取り、型(f a)を返す関数である。ここで、fはFunctorクラスのインスタンス、aはNumクラスのインスタンスである。
関数「f :: a -> a」にfmapを適用することにより、関数「g :: f a -> f a」を得ることができました。
これの何が嬉しいのか、イメージが湧きづらいと思いますので、もう少し詳しく見てみます。
fmapが適用された関数は、どのFunctorに関しても利用できる
上記の通り、関数「f :: a -> a」にfmapを適用することにより、関数「g :: f a -> f a」を得ることができました。
関数gの型をもう一度見てみます。
g :: (Functor f, Num a) => f a -> f a --ここでは、「g = fmap f」として束縛されている。
型を見てわかる通り、引数のfに対して、「Functor」であることという制約を課していますが、それ以上の制約は課していません。なので、上記fに対して、どんなFunctorでも適用できるということになります。
例を見てみましょう。
※以下のMaybe、およびEither StringはいずれもFunctor則を満たすfmapを提供するインスタンスです
関数gにMaybe Int型を適用してみる
Prelude> :t g $ (Just 1 :: Maybe Int) --gにMaybe Int型の値を適用した結果の型を調べる
g $ (Just 1 :: Maybe Int) :: Maybe Int --結果は、Maybe Intとなる。fの部分にMaybe、aの部分にIntが適用された形となる。
Prelude> g $ (Just 1 :: Maybe Int) --実際に関数gにMaybe Int型である「Just 1」を適用
Just 2 --「g = fmap f = (+1)」なので、結果は「Just 2」となる。
関数gにEither String Int型を適用してみる
Prelude> :t g $ (Right 1 :: Either String Int) --gにEither String Int型の値を適用した結果の型を調べる
g $ (Right 1 :: Either String Int) :: Either String Int --結果は、Either String Intとなる。fの部分にEither String、aの部分にIntが適用された形となる。
Prelude> g $ (Right 1 :: Either String Int) --実際に関数gにEither String Int型である「Just 1」を適用
Right 2 --「g = fmap f = (+1)」なので、結果は「Right 2」となる。
上記の通り、関数fをfmapによる持ち上げにより束縛した関数gは、引数としてMaybe Int、Either String Intを取っています。
もうちょっとFunctorの何が嬉しいか考えてみる
これまでで、fmapの適用により持ち上がられた関数gは、どのFunctorインスタンスに対しても引数として取れるようになることがわかりました。ここで、視点を変えてみます。
改めて今回使用した関数fと関数gの定義を見てみましょう。
let f = (+1)
let g = fmap f
fmapにより関数gを定義したとしても、「f = (+1)」の定義はそのままです。
これは、「関数fという特定のFunctorインスタンスに依存しない関数の定義はそのままで、fmapの適用により、どのFunctorインスタンスも引数に取れる関数を新たに作ることができる」と考えることができます。
これは素晴らしい。
例えば、開発の初期に以下のような関数を作ったとします。
add1 :: Int -> Int
add1 = (+1)
当初、上記のadd1関数は引数としてFunctorに依存しない汎用的な関数として作っていたとしましょう。
ところが、開発の途中、引数としてMaybe Intをとる以下のような関数を使いたくなったとします。
maybeAdd1 :: Maybe Int -> Maybe Int
maybeAdd1 n = case n of
Just n -> Just (n + 1)
Nothing -> Nothing
maybeAdd1の計算を実現するために、当初定義していたadd1関数を作り直す必要があるのでしょうか。
また、maybeAdd1のような関数を再定義する必要があるのでしょうか。
その必要はありません。これまで見てきたように、fmapを使ってadd1関数を持ち上げてやれば解決です。
*Main>let fAdd1 = fmap add1 -- add1関数をfmapにより、Functorのコンテキストで使えるよう持ち上げ
*Main>:t fAdd1 --fmapによる持ち上げにより作られたfAdd1関数の型を表示
fAdd1 :: Functor f => f Int -> f Int --add1関数をFunctorのコンテキストで使えるようになった
*Main> fAdd1 $ Just 1 -- fAdd1関数にJust 1を与える
Just 2 --結果はJust 2となる。
*Main> fAdd1 $ Nothing --fAdd1関数にNothingを与える
Nothing --結果はNothingとなる。
特定のコンテキストに依存しない関数をfmapにより、Functorのコンテキストで使えるようにする、というのはオブジェクト指向に慣れ親しんだプログラマからすると、なかなか目から鱗ではないでしょうか。私もそうでした。
まだまだ筆者も勉強中ですが、次は時間があったら、Applicativeとか取り上げたいと思います。