Functorを理解する
この記事ではFunctorというHaskellには欠かせない型クラスについて、概念や捉え方、どういった型がFunctorという振る舞いをできるのかを解説します。
型クラスとは
型クラスとは、値の等値性を判定したり、ある値を文字列として表現したりと特定の振る舞いを定義するインターフェイスです。その特定の振る舞いを定義した関数のことをメソッド、そのメソッドを実装した型をその型クラスのインスタンスと呼びます。
状態という箱
値を関数に適用したいときは次のように書けます。
> (+100) 5 105
Haskellではある状態に値が入っていることがよくあります。例えばMaybe型は値がJust 5
に入っていたりするように。このある状態は値を入れる「箱」と考えることができます。
ここでMaybe型に入った値を関数に適用したいとします。しかし、次のように書いてもうまくいきません。
> (+100) (Just 5) <interactive>:5:1: Non type-variable argument in the constraint: Num (Maybe a) ...
これはMaybeという箱に値が入ったままで、適用したい関数がその値の取り出し方を知らないからです。そこで活躍するのがFunctorという型クラスとfmapという関数です。
Functorとは
Functorは型クラスです。
早速ですがFunctorの型クラスの定義を見てみましょう。Data.Functorに定義されています。
class Functor f where fmap :: (a -> b) -> f a -> f b
https://hackage.haskell.org/package/base-4.9.1.0/docs/src/GHC.Base.html#Functor
fmapという関数が定義されており、この関数は「aを取りbを返す関数」とf aを取り、f bを返します。ここでいうfはファンクター値と呼ばれFunctorのインスタンスとなる型が入ります。
そして、MaybeはFunctorのインスタンスなので当然fmapを実装しています。このときのfmapの型シグネチャは次のようになります。
fmap :: (a -> b) -> Maybe a -> Maybe b
ちょうどファンクター値fがインスタンスであるMaybeに置き換わった感じですね。 ついでにインスタンス定義も見てみましょう。
instance Functor Maybe where fmap f Nothing = Nothing fmap f (Just a) = Just (f a)
https://hackage.haskell.org/package/base-4.9.1.0/docs/src/GHC.Base.html#line-652
fmapにはパターンマッチが使われていて箱に入った値がJustかNothingで挙動が違います。Justの場合は箱から値を取り出して関数fに適用してから再度Justに入れ直して返します。一方でNothingの場合は箱には何もせずにそのままNothingのみを返します。
それでは実際に使ってみましょう。
> fmap (+100) (Just 5) Just 105 > fmap (+100) Nothing Nothing
うまくいきました。Nothingの場合は箱に値も何も入っていないことと同じなので、何もないものに関数を適用しても何も変化がないのは当然と言えるでしょう。
fmapは中置演算子( <$>
)としても提供されています。
> (+100) <$> (Just 5) Just 105
Functorのインスタンスたち
Maybe以外にもFunctorのインスタンスになれる型があります。
リスト
リストもFunctorです。リストという箱には複数の値が入りますが、リストのfmapは箱から値を1つずつ取り出して関数に適用します。
> fmap (+100) [1..5] [101,102,103,104,105]
この動作はmap関数と同じものなので、インスタンス定義も次のようになっています。
instance Functor [] where fmap = map
https://hackage.haskell.org/package/base-4.9.1.0/docs/src/GHC.Base.html#line-734
Either
EitherはMaybeと似ていますが、MaybeのNothingと違い失敗を表したいときにも値を付けられます。
data Either a b = Left a | Right b
Maybeファンクターと同じく箱に入った値には何もせずそのまま返します。
> fmap (+100) (Right 5) Right 105 > fmap (+100) (Left "error...") Left "error..."
Eitherは型変数を2つ取るのでインスタンス定義が他のものと少し違います。Functorは型クラス定義から分かるように1つの型変数しか取らないので、Eitherのような型変数を複数持つ型をインスタンスにしたい場合はFunctor (Either a)
のように部分適用により型変数の数を調整する必要があります。
instance Functor (Either a) where fmap _ (Left x) = Left x fmap f (Right y) = Right (f y)
https://hackage.haskell.org/package/base-4.9.1.0/docs/src/GHC.Base.html#line-127
この定義が正しいことはfmapの型シグネチャにファンクター値を実際に代入してみることで確かめることができます。
元のfmapの型シグネチャはこうでした。
fmap :: (a -> b) -> f a -> f b
そしてEitherファンクターのファンクター値はEither a
なので、fmapの型シグネチャは次のようになります。
fmap :: (b -> c) -> Either a b -> Either a c
仮にファンクター値がEither a b
だった場合は次のようになりますが、Either a b c
は最早Either型でないことが分かります。
fmap :: (c -> d) -> Either a b c -> Either a b d
関数
今まで我々が何気なく使っていた関数もまたFunctorです。実際に試してみましょう。
> let a = fmap (*2) (+100) > :t a a :: Num b => b -> b > a 5 210
値として渡した関数に関数を適用しようとすると、どうやら新しい関数を返すようです。これは「箱から値を取り出して関数に適用し、その値をまた箱に入れ直す」というFunctorの性質と合っているように見えます。
新しく返ってきた関数は引数を1つ取り、(+100)
,(*2)
の順で適用して値を返します。この処理は合成関数(.
)でやっていることと同じです。
instance Functor ((->) r) where fmap = (.)
https://hackage.haskell.org/package/base-4.9.1.0/docs/src/GHC.Base.html#line-638
この他にもいろんな型がありますが、ファンクターとは別の概念の説明が必要になるのでまたの機会に紹介します。