Haskellを書く上で、避けては通れない概念がいくつかあります。Functor(ファンクタ)もその1つですが、特徴的なものなので、理解しにくい人もいると思います。今回はできるだけコード例と図を示しながら解説していきます。後半ではFunctorを扱うライブラリのData.Functorについて見ていきます。
注意
・数学の圏論には詳しくないです。そのため、関手(数学のFunctor)についての話はできません。
・この記事で使用しているGHCのバージョンは8.8.3です。
最初のFunctor
はっきり言うと、Functorは型クラスです。GHC.Base
で定義されていて、Preludeにも含まれています。定義はこうです。
class Functor (f :: * -> *) where
fmap :: (a -> b) -> f a -> f b
(<$) :: a -> f b -> f a
{-# MINIMAL fmap #-}
MINIMAL fmap
とあるので、何かしらの型をFunctorのインスタンスにしたければfmap
のみを定義すればよい、とのことです。
* -> *
がFunctorのキモです。型注釈に似ていますが、これはカインドについての注釈です。Int
やChar
、Bool
のカインドは*
つまり値そのものなのでFunctorのインスタンスにはなれません。GHCiで:info
を使って型宣言を、:kind
でカインドを見られます。見てみましょう。
Prelude> :info Int
data Int = GHC.Types.I# GHC.Prim.Int# -- Defined in ‘GHC.Types’
...
Prelude> :info Char
data Char = GHC.Types.C# GHC.Prim.Char# -- Defined in ‘GHC.Types’
...
Prelude> :info Bool
data Bool = False | True -- Defined in ‘GHC.Types’
...
Prelude> :kind Int
Int :: *
Prelude> :kind Char
Char :: *
Prelude> :kind Bool
Bool :: *
型宣言のあとにその型がどのインスタンスになっているかが表示されます。ここでは型宣言だけを見るためにそれらは省略しました。3つの型に共通するのは、どれも型コンストラクタが引数をとらないということ、すなわち値そのものということです。
ここで、Functorのインスタンスで私たちにも馴染み深いMaybeの型宣言を見てみましょう。
data Maybe a = Nothing | Just a -- Defined in ‘GHC.Maybe’
Maybe a
というのは、a
という型を引数にとるよ、という意味です。型コンストラクタが引数をとって、Nothing
かJust a
の型になるよ、と言っています。なんだか関数みたいですね。カインドも見てみましょう。
Maybe :: * -> *
型引数を1つとって何かしらの値を「返す」型だということです。このカインドはFunctorのインスタンス* -> *
に一致するので、Functorになれます。
Functorになるためのfmap
は、面白い型宣言をしています。
fmap :: (a -> b) -> f a -> f b
fmap
は関数と、Functorに包まれた値を1つとって、関数を中の値に適用して再び包んだものを返します。図にするとこうです。
fmap
は、その内部で下のように関数を適用しています。
MaybeはGHC.Base
でFunctorのインスタンスとして定義されています。
instance Functor Maybe where
fmap _ Nothing = Nothing
fmap f (Just a) = Just (f a)
fmap
でMaybeの値に関数を適用すると、Nothingなら取り出せる値がないのでNothingを、Justなら中の値に関数を適用してJustに包んで返す、ということがわかります。このとき、fmap
の型注釈は次のようになると予想できます。
fmap :: (a -> b) -> Maybe a -> Maybe b
さっそく使ってみましょう。
Prelude> fmap (*3) (Just 8)
Just 24
Prelude> fmap (*3) Nothing
Nothing
fmap
を使って、包まれた値に関数を適用できるというわけです。
ファンクタ則
Functorのインスタンスになるためには、その動作がファンクタ則と呼ばれるルールを満たしていなければなりません。ルールは以下の2つです。
fmap id = id
fmap (f . g) == fmap f . fmap g
つまり、仮にFunctorのインスタンスSomeType
があるとき、次の2つの式はTrue
にならなければならない、ということです。
fmap id (SomeType x) == id (SomeType x)
fmap (f . g) (SomeType x) == (fmap f . fmap g) (SomeType x)
その1
fmap id = id
この法則は、id
をfmap
で中の値に適用したときに中の値が変わってはならない、というものです。
前提として、id
は引数をそのまま返します。図にするとこうです。
id :: a -> a
fmap id
の挙動は、こうなると予想できます。
fmap id :: Functor f => f a -> f a
Maybe
でこれを確認してみましょう。
ghci> fmap id (Just 2)
Just 2
ghci> id (Just 2)
Just 2
ghci> fmap id Nothing
Nothing
ghci> id Nothing
Nothing
ちゃんと満たしていますね。
その2
fmap (f . g) == fmap f . fmap g
これは、関数合成を行ったf . g
をfmap
するのは、fmap g
に続けてfmap f
をしたものと等しい、という意味です。
前提として、f
とg
は次のような関数とします。
f :: a -> b
g :: b -> c
f . g :: a -> c
fmap f . g
は次のように動作するはずです。
fmap f . g :: Functor f => f a -> f c
そして、fmap f . fmap g
はこう動作するはずです。
fmap f . fmap g :: Functor f => f a -> f c
Maybe
を使って確認してみます。
ghci> fmap ((+2) . (*3)) (Just 30)
Just 92
ghci> (fmap (+2) . fmap (*3)) (Just 30)
Just 92
ghci> fmap ((+2) . (*3)) Nothing
Nothing
ghci> (fmap (+2) . fmap (*3)) Nothing
Nothing
等しい値になっていることがわかります。
以上の2つから、Maybe
はファンクタ則を満たしていることがわかりました。この後に紹介するFunctorたちもこの2つの法則を満たしています。
その他のFunctorたち
Maybe以外にも、Functorとして定義されているものがいくつかあります。
リスト
リストといってもFunctorになれるのは空リストです。リストは非決定性計算としても扱われるので、ある意味で値を包んだものと言えます。
instance Functor [] where
fmap = map
fmap
はmap
であると定義されています。
map
はリストの各要素に関数を適用するので、fmap
に求められる動作としては間違っていません。fmap
をmap
で書き直すとこうなります。
instance Functor [] where
map _ [] = []
map f (x:xs) = f x : map xs
リストが空ならそのまま返し、そうでなければ関数を適用して返す、という動作です。もちろん、これはファンクタ則を満たしています。
fmap id [x1, x2, ..]
= [x1, x2, ..]
= id [x1, x2, ..]
fmap (f . g) [x1, x2, ..]
= (f . g) [x1, x2, ..]
= fmap f (g [x1, x2, ..])
= (fmap f . fmap g) [x1, x2, ..]
ghci> fmap id []
[]
ghci> fmap (*2) []
[]
ghci> (fmap (*2) . fmap (+10)) [1..10]
[22,24,26,28,30,32,34,36,38,40]
ghci> fmap ((*2) . (+10)) [1..10]
[22,24,26,28,30,32,34,36,38,40]
タプル
GHCでは、2~4要素のタプルまでがFunctorのインスタンスとして定義されています。
instance Functor ((,) a) where
fmap f (x,y) = (x, f y)
instance Functor ((,,) a b) where
fmap f (a, b, c) = (a, b, f c)
instance Functor ((,,,) a b c) where
fmap f (a, b, c, d) = (a, b, c, f d)
どのファンクタも、最後の要素にのみ関数が適用されます。この実装は、ファンクタ則を満たしています。
fmap id (x, y)
= (x, id y)
= (x, y)
= id (x, y)
fmap (f . g) (x, y)
= (x, (f . g) y)
= fmap f (x, g y)
= (fmap f . fmap g) (x, y)
もちろん、ペア以外のタプルでも同様です。
Either
Eitherは2つの型引数をとり、LeftかRightで包んで返す型です。1
data Either a b = Left a | Right b
失敗する可能性のある計算を行う時に使い、Leftは失敗したときの値、Rightは成功したときの値です。ただ、Either
のままだと型コンストラクタは* -> * -> *
なので、Functorのインスタンスになれません。なので、a
のみを部分適用して* -> *
の形にします。
instance Functor (Either a) where
fmap _ (Left x) = Left x
fmap f (Right y) = Right (f y)
ファンクタ則を満たしているという確認は一瞬です。型引数はb
なので、b
に関数が適用されるのがポイントです。
fmap id (Either a (b))
= Either a (id b)
= Either a b
= id (Either a b)
fmap (f . g) (Either a (b))
= Either a ((f . g) b)
= fmap f (Either a (g b))
= (fmap f . fmap g) (Either a b)
IO
IOは、I/Oアクションつまり動作とその結果を扱う型です。
instance Functor IO where
fmap f x = x >>= (pure . f)
この時、fmap
は包まれた値に関数を適用するため、その型はこうなるはずです。
fmap :: (a -> b) -> IO a -> IO b
注意してほしいのは、戻り値はIOに包まれているということです。
>>=
とpure
の型は次のようになります。
(>>=) :: Monad m => m a -> (a -> m b) -> m b
pure :: Applicative f => a -> f a
>>=
は動作の結果を取り出して関数に渡し、pure
は文脈に合う値を生成します。これを踏まえると、fmap
はx
から値を取り出してf
を適用し、pure
で文脈に合う値(=IO
)を返すことがわかります。
I/Oアクションの例としては、getLine
やputStrLn
があります。
getLine :: IO String
putStrLn :: String -> IO ()
getLine
はユーザからの入力を受け取り、IOで包んだ文字列として返します。putStrLn
は文字列を受け取り、文字列を出力する動作を返します。
IOをFunctorにするメリットは、IO
の殻をあまり意識せずにI/Oアクションから値を操作できることです。つまり、プログラムの純粋な部分とそうでない部分を分離できます。
サンプルとして、プログラムの引数を改行で区切って表示するプログラムを書いてみます。コマンドライン引数をとる関数としてgetArgs
をインポートします。unlines
を使って改行で引数をつなぎます。
getArgs :: IO [String]
unlines :: [String] -> String
下がプログラムの本体です。
import System.Environment (getArgs)
main = putStrLn =<< unlines <$> getArgs
getArgs
で受け取ったコマンドライン引数のリストから値を取り出し、unlines
を適用します。<$>
はfmap
の中置演算子バージョンです。それを先程の=<<
でputStrLn
に突っ込んで出力しています。
関数
関数、つまり((->) r)
は、Functorの一種です。Functorは、包まれた値の世界f a
での写像2とも言えます。インスタンス実装の前に、fmap
の型を考えてみます。Functor f
の部分に((->) r)
がくるので、下のようになると思われます。
fmap :: (a -> b) -> (r -> a) -> (r -> b)
fmap
が(a -> b)
と(r -> a)
をくっつけているように見えませんか?
instance Functor ((->) r) where
fmap = (.)
((->) r)
は出力の欠けた関数なので、カインドは* -> *
になります。もちろん、この実装はファンクタ則を満たします。
fmap id f
= f . id
= f
= id f
fmap (g . h) f
= (g . h) . f
= fmap g (h . f)
= (fmap g . fmap h) f
関数のファンクタにfmap
することは、関数合成をすることと等しいことがわかります。
Functorのインスタンスを作ってみる
いくつかFunctorを見てきたので、そろそろFunctorのインスタンスを自作してみましょう。今回インスタンスにするのは次のような再帰的な型です。
infixr 5 :::
data List a = None | List a ::: a
deriving (Show)
目標は、fmap
が(a -> b) -> List a -> List b
になるように実装することです。早速やってみましょう。
instance Functor List where
fmap _ None = None
fmap f (as ::: a) = fmap f as ::: f a
ghci> fmap (*2) (None:::1:::2:::3:::4:::5)
None ::: (2 ::: (4 ::: (6 ::: (8 ::: 10))))
ghci> fmap sum (None ::: [1..5]:::[1..10]:::[1..20])
None ::: (15 ::: (55 ::: 210 ))
ちゃんと動いています!!
Data.Functor
続いては、Functorに関連する関数をまとめたData.Functorモジュールを見ていきます。次のような関数群があります。
($>) :: Functor f => f a -> b -> f b
(<$>) :: Functor f => (a -> b) -> f a -> f b
(<&>) :: Functor f => f a -> (a -> b) -> f b
void :: Functor f => f a -> f ()
fmap :: Functor f => (a -> b) -> f a -> f b
(<$) :: Functor f => a -> f b -> f a
fmap, (<$>), (<&>)
fmap
はもうおなじみですね。<$>
はfmap
の中置演算子バージョンで、<&>
はその引数を入れ替えたものです。fmap
はインスタンスに依存しますが、<$>
と<&>
はData.Functorで実装されています。
infixl 4 <$>
(<$>) :: Functor f => (a -> b) -> f a -> f b
(<$>) = fmap
infixl 1 <&>
(<&>) :: Functor f => f a -> (a -> b) -> f b
as <&> f = f <$> as
どちらの実装も、すんなり理解できると思います。<$>
はfmap
そのもの、<&>
は関数と包まれた値を入れ替えたものですね。どちらも左結合で、<&>
の優先順位は下から2番目で、$
の1つ上なので、<$>
より比較的後に評価されます。関数合成.
はinfixr 9
ですから、関数を書き連ねたいときは<&>
を、そうでなければ<$>
を使うとよいかと思います。
(<$), ($>)
(<$) :: Functor f => a -> f b -> f a
($>) :: Functor f => f a -> b -> f b
<$
は、右手のファンクタの値からファンクタのみを取り出して左手の値に被せる演算子です。$>
はその逆です。
infixr 4 <$
(<$) :: a -> f b -> f a
(<$) = fmap . const
infixr 4 $>
($>) :: Functor f => f a -> b -> f b
($>) = flip (<$)
ghci> "abc" <$ Just 100
Just "abc"
ghci> "abc" <$ Nothing
Nothing
ghci> "abc" <$ Left 100
Left 100
ghci> "abc" <$ Right 100
Right "abc"
ghci> "abc" <$ [1..10]
["abc","abc","abc","abc","abc","abc","abc","abc","abc","abc"]
ghci> "abc" <$ (100,200)
(100,"abc")
ghci> Just "abc" <$ Just 100
Just (Just "abc")
void
void :: Functor f => f a -> f ()
void
はファンクタの値をとり、ファンクタを()
(ユニット)に被せて返します。
ghci> void Just 100
Just ()
ghci> void Nothing
Nothing
ghci> void [1..10]
[(),(),(),(),(),(),(),(),(),()]
また、IOアクションの戻り値を捨てられます。
ghci> mapM print "abc"
'a'
'b'
'c'
[(),(),()]
ghci> void $ mapM print "abc"
'a'
'b'
'c'
まとめ
- Functorは何かしらの包まれた値を扱う型クラスです
- Functorのインスタンスはファンクタ則を満たします
- Haskellの「失敗を扱う型」や「状態を扱う型」(要するに値を包む型)はFunctorのインスタンスになっています
感想
個人的には、Functorはやっぱり包まれた値と相性がいいと思います。