M.Hiroi's Home Page

お気楽 Haskell プログラミング入門

中級編 : Functor

Copyright (C) 2013-2021 Makoto Hiroi
All rights reserved.

はじめに

今回は Haskell の Functor (ファンクタ) について説明します。ファンクタというと、SML/NJ や OCaml ユーザーにはお馴染み機能だと思います。これら ML 系言語のファンクタは、ストラクチャを受け取って新しいストラクチャを生成する機能ですが、Haskell のファンクタはこれらとはまったく異なる機能です。

といっても難しいことは全然なく、Haskell では単なる「型クラス」のひとつにすぎません。データ型を Functor のインスタンスに設定すると、リスト以外のデータ型でも map と同様の働きをさせることができます。

●Functor とは?

Functor の定義をコマンド :info で調べると、次のように表示されます。

ghci> :i Functor
type Functor :: (* -> *) -> Constraint
class Functor f where
  fmap :: (a -> b) -> f a -> f b
  (<$) :: a -> f b -> f a
  {-# MINIMAL fmap #-}
        -- Defined in ‘GHC.Base’
instance Functor ((,) a) -- Defined in ‘GHC.Base’
instance Functor ((,,) a b) -- Defined in ‘GHC.Base’
instance Functor ((,,,) a b c) -- Defined in ‘GHC.Base’
instance Functor ((,,,,) a b c d) -- Defined in ‘GHC.Base’
instance Functor ((,,,,,) a b c d e) -- Defined in ‘GHC.Base’
instance Functor ((,,,,,,) a b c d e f) -- Defined in ‘GHC.Base’
instance Functor ((->) r) -- Defined in ‘GHC.Base’
instance Functor IO -- Defined in ‘GHC.Base’
instance Functor [] -- Defined in ‘GHC.Base’
instance Functor Maybe -- Defined in ‘GHC.Base’
instance Functor Solo -- Defined in ‘GHC.Base’
instance Functor (Either a) -- Defined in ‘Data.Either’

ここで、関数 fmap に注目してください。これが Functor の主な関数です。最初の引数が関数 a -> b で、a を b に変換する関数であることがわかります。次の引数 f a は型変数 a をひとつ持っているので、f は a を格納するデータ型 (型構築子) であることがわかります。そして、最後の引数が f b なので、f a からデータを取り出し、それを関数 a -> b に適用し、その結果を f に格納して返すことがわかります。

fmap の定義は map の定義と良く似ています。

map :: (a -> b) -> [a] -> [b]

リストの型 [a] は [ ] a のことなので、次のように書き直すことができます。

map :: (a -> b) -> [] a -> [] b

つまり、fmap は汎用のマップ関数なのです。簡単な実行例を示しましょう。

ghci> fmap (*2) [1,2,3,4,5]
[2,4,6,8,10]
ghci> fmap (*2) []
[]
ghci> fmap (*2) (Just 10)
Just 20
ghci> fmap (*2) Nothing
Nothing
ghci> fmap reverse getLine
hello, world
"dlrow ,olleh"

リストの Functor は関数 map と同じ動作になります。Maybe の Functor は、Just からデータを取り出して、それに関数を適用して返り値を Just に格納して返します。Nothing はデータを格納していないので Nothing をそのまま返します。

getLine のデータ型は IO String です。Haskell では IO も Functor のインスタンスで、IO に格納されているデータを取り出して、それに関数適用して返り値を IO に格納して返します。

なお、Haskell には fmap と同じ働きをする演算子 <$> も定義されています。

Prelude> :t (<$>)
(<$>) :: Functor f => (a -> b) -> f a -> f b
ghci> :t fmap
fmap :: Functor f => (a -> b) -> f a -> f b
ghci> (*2) <$> Just 10
Just 20
ghci> (*2) <$> [1,2,3,4,5]
[2,4,6,8,10]

Haskell では「文脈」という用語を使うことがあります。たとえば、Maybe の Nothing は計算が失敗したことを表わしています。Maybe は失敗するかもしれない計算を表している、と考えることができるわけです。リストにはリストの意味を考えることができますし、I/O アクションもその意味を考えることができます。このようなプログラム上の意味を文脈といいますが、ここでは値を格納している型構築子のことを文脈と考えてもらってもかまいません。

fmap は文脈が表している値に、文脈を保ったまま関数を適用する働きをします。fmap に渡される関数は文脈に依存していません。文脈に合わせて関数を適用することを「関数を持ち上げる」といいます。fmap は関数を持ち上げて文脈の値に適用し、文脈を保ったままその結果を返すわけです。ちょっと耳慣れない用語で難しく感じるかもしれませんが、実際にプログラムを作っていくうちに慣れてくると思います。

●自分で Functor を定義する

それではここで Functor の理解を深めるため、私たちで Functor を定義してみましょう。型クラスの名前は Mfunctor とします。fmap は Haskell で使われているので fmap' としました。Mfunctor の定義は次のようになります。

リスト : Mfunctor の定義

class Mfunctor f where
  fmap' :: (a -> b) -> f a -> f b

Mfunctor の定義は関数名が異なるだけで Functor と同じです。

次はリスト, Maybe, IO を Mfunctor のインスタンスに設定します。次のリストを見てください。

リスト : インスタンスの設定

instance Mfunctor [] where
  fmap' = map

instance Mfunctor Maybe where
  fmap' f Nothing  = Nothing
  fmap' f (Just x) = Just (f x)

instance Mfunctor IO where
  fmap' f action = do
    x <- action
    return (f x)

リストの fmap' は map と同じです。Maybe の場合、右辺が Nothing であれば Nothing をそのまま返し、Just x であれば Just (f x) を返します。IO の fmap' は do 構文を使うと簡単に実装できます。I/O アクション action から <- でデータ x を取り出し、f x の返り値を return で IO に格納して返します。

それでは実行してみましょう。

ghci> fmap' (*2) [1,2,3,4,5]
[2,4,6,8,10]
ghci> fmap' (*2) []
[]
ghci> fmap' (*2) (Just 10)
Just 20
ghci> fmap' (*2) Nothing
Nothing
ghci> fmap' reverse getLine
hello, world
"dlrow ,olleh"

●Either

次は Either というデータ型で試してみましょう。Either の定義を示します。

リスト : Either の定義

data Either a b = Left a | Right b

Either は Maybe を改良したデータ型で、左 (Left) 右 (Right) のどちらかにデータを格納することができます。左右のデータ型は異なっていてもかまいません。通常は Maybe の Nothing の代わりに Left a を使い、Just の代わりに Right b を使います。たとえば、エラーがあったときに Left にエラーを表すデータを入れて返す、という使い方をします。

簡単な例を示します。

ghci> Right 10
Right 10
ghci> :t Right 10
Right 10 :: Num b => Either a b

ghci> Left "error"
Left "error"
ghci> :t Left "error"
Left "error" :: Either [Char] b

●Either も Functor になる

Either は Maybe と同じ考え方で Functor のインスタンスにすることができます。つまり、Left のデータであればそのまま返し、Right のデータであれば関数を適用します。プログラムは次のようになります。

リスト : Either の Functor

instance Mfunctor (Either a) where
  fmap' f (Left x)  = Left x
  fmap' f (Right x) = Right (f x)

fmap' の定義は簡単ですね。Left x ならば Left x をそのまま返し、Right x ならば Right (f x) を返します。ここで Either の型に注目してください。Either は型変数を 2 つ指定しますが、fmap' の定義は (a -> b) -> f a -> f b なので、型構築子 f で指定できる型変数はひとつだけになります。このため、instance 宣言で Either または (Either a b) と指定するとエラーになります。

Haskell の場合、型構築子における型変数の指定は、カリー化関数のように部分適用することができます。Either の Functor は Right のデータに関数を適用するので、データ型が変化することがありますが、Left のデータ型は変化しません。この場合、Either a を型構築子 f と考えればいいのです。これを図に示すと次のようになります。

fmap' :: (a -> b) -> f a -> f b
=> (a -> b) -> (Either a') a -> (Either a') b
=> (a -> b) -> Either a' a -> Either a' b

これで Right 側のデータに関数が適用され、Left 側のデータは変化しないことを表すことができます。

簡単な実行例を示します。

ghci> fmap' (*2) (Right 10)
Right 20
ghci> fmap' (*2) (Left "error")
Left "error"

●連想リストも Functor になる

拙作のページ「データ型の定義」で作成した連想リストも Functor のインスタンスにすることができます。次のリストを見てください。

リスト : 連想リストの Functor

-- 連想リストの定義
data Alist k v = Nil | Cell k v (Alist k v) deriving Show

-- Functor
instance Mfunctor (Alist k) where
  fmap' f Nil = Nil
  fmap' f (Cell k v next) = Cell k (f v) (fmap' f next)

連想リストの型は Alist k v なので、Either と同じようにキーはそのままとして、データ (値) に関数を適用することにします。したがって、instance 宣言では型を (Alist k) と指定します。あとは関数 f を値 v に適用して新しい連想リストを生成するだけです。

それでは実行してみましょう。

ghci> a = Cell 'a' 1 (Cell 'b' 2 (Cell 'c' 3 Nil))
ghci> a
Cell 'a' 1 (Cell 'b' 2 (Cell 'c' 3 Nil))
ghci> fmap' (*2) a
Cell 'a' 2 (Cell 'b' 4 (Cell 'c' 6 Nil))
ghci> fmap' (+10) a
Cell 'a' 11 (Cell 'b' 12 (Cell 'c' 13 Nil))

●関数も Functor になる

今まで関数の型は r -> a で表していました。Haskell の場合、-> は中置演算子で、関数の型は (->) r a と書くこともできます。Either と同じように、(->) r を型構築子と考えると、次のように fmap' の定義を導くことができます。

fmap' :: (a -> b) -> f a -> f b
=> (a -> b) -> (->) r a -> (->) r b
=> (a -> b) -> (r -> a) -> (r -> b)

これは関数合成を行う演算子 (.) の定義とまったく同じです。

(.) :: (b -> c) -> (a -> b) -> a -> c

つまり、関数の Functor は関数合成と同じ動作でよいわけです。プログラムは次のようになります。

リスト : 関数の Functor

instance Mfunctor ((->) r) where
  fmap' = (.)

簡単な実行例を示します。

ghci> fmap' (fmap' (*2) (+3)) (Just 10)
Just 26
ghci> fmap' (fmap' (*2) (+3)) Nothing
Nothing
ghci> fmap' (fmap' (*2) (+3)) [1..5]
[8,10,12,14,16]
ghci> fmap' (fmap' (*2) (+3)) [1]
[8]
ghci> fmap' (fmap' (*2) (+3)) []
[]
ghci> fmap' (fmap' (*2) (+3)) (Right 20)
Right 46
ghci> fmap' (fmap' (*2) (+3)) (Left "error")
Left "error"
ghci> fmap' (fmap' (*2) (+3)) (Cell 'a' 10 (Cell 'b' 20 (Cell 'c' 30 Nil)))
Cell 'a' 26 (Cell 'b' 46 (Cell 'c' 66 Nil))

fmap' (*2) (+3) で関数を合成して関数 (x + 3) * 2 を作り、その関数をデータに適用します。Just 10 は Just 26 に、[1,2,3,4,5] は [8,10,12,14,16] に、Right 20 は Right 46 になります。もちろん、連想リストに適用することもできます。このように、Functor を定義すると、いろいろなデータ構造に関数を適用していくことができます。

●Functor の規則

Functor には満たすべき規則が 2 つあります。

  1. fmap id == id
  2. fmap (f . g) == fmap f . fmap g

最初の規則は、データに fmap id を適用した結果は id を適用した結果と等しい、という意味です。つまり、fmap は関数をデータに適用するだけで、他の操作は一切行わない、ということです。

2 番目の規則は引数を与えて考えるとわかりやすくなります。

fmap (f . g) x = fmap f (fmap g x)

fmap (f . g) を x に適用した結果は、fmap g を x に適用してから fmap f を適用した結果と同じになります。左辺は fmap を 1 回だけ、右辺は fmap を 2 回適用しています。たとえば、fmap がリストの要素を逆に並べ替えるとしましょう。当然ですが、右辺と左辺では結果が異なりますね。つまり、fmap によって要素の並び方は変更されない、ということを意味しています。

なお、Functor の規則は Haskell が自動的にチェックしてくれるわけではありません。Functor を作ったならば、規則を満たしているかプログラマ自らが確かめる必要があります。

それでは規則を満たしているか実際に試してみましょう。

ghci> fmap' id (Just 10)
Just 10
ghci> fmap' id Nothing
Nothing
ghci> fmap' id [1,2,3,4,5]
[1,2,3,4,5]
ghci> fmap' id []
[]
ghci> fmap' id (Right 10)
Right 10
ghci> fmap' id (Left "error")
Left "error"
ghci> fmap' id (Cell 'a' 10 (Cell 'b' 20 (Cell 'c' 30 Nil)))
Cell 'a' 10 (Cell 'b' 20 (Cell 'c' 30 Nil))
ghci> fmap' id Nil
Nil

最初の規則は満たしているようですね。次は 2 番目の規則を満たしているかチェックしてみましょう。

ghci> fmap' ((+2) . (*3)) (Just 10)
Just 32
ghci> fmap' (+2) (fmap' (*3) (Just 10))
Just 32
ghci> fmap' ((+2) . (*3)) Nothing
Nothing
ghci> fmap' (+2) (fmap' (*3) Nothing)
Nothing
ghci> fmap' ((+2) . (*3)) (Right 10)
Right 32
ghci> fmap' (+2) (fmap' (*3) (Right 10))
Right 32
ghci> fmap' ((+2) . (*3)) (Left "error")
Left "error"
ghci> fmap' (+2) (fmap' (*3) (Left "error"))
Left "error"
ghci> fmap' ((+2) . (*3)) [1,2,3,4,5]
[5,8,11,14,17]
ghci> fmap' (+2) (fmap' (*3) [1,2,3,4,5])
[5,8,11,14,17]
ghci> fmap' ((+2) . (*3)) (Cell 'a' 10 (Cell 'b' 20 (Cell 'c' 30 Nil)))
Cell 'a' 32 (Cell 'b' 62 (Cell 'c' 92 Nil))
ghci> fmap' (+2) (fmap' (*3) (Cell 'a' 10 (Cell 'b' 20 (Cell 'c' 30 Nil))))
Cell 'a' 32 (Cell 'b' 62 (Cell 'c' 92 Nil))

2 番目の規則も満たしていますね。

●Functor による関数の部分適用

Functor で関数の部分適用を行うこともできます。この場合、部分適用で生成された関数はデータ型の中に格納されます。たとえば、関数 a -> b -> c は a -> (b -> c) のことですから、fmap に適用すると次のようになります。

fmap :: (a -> b) -> f a -> f b
fmap (a -> b -> c) => (a -> (b -> c)) -> f a -> f (b -> c)

ただし、生成された関数は fmap で実行することができません。なぜなら、関数はデータ型 f の中に格納されているからです。簡単な例を示しましょう。

ghci> :t fmap (+) (Just 2)
fmap (+) (Just 2) :: Num a => Maybe (a -> a)
ghci> :t fmap (+) [1,2,3,4,5]
fmap (+) [1,2,3,4,5] :: Num a => [a -> a]

最初の例は関数 (+2) が生成されますが、その関数は Maybe 型の中に格納されます。リスト [1, 2, 3, 4, 5] の場合は、関数 (+1), (+2), (+3), (+4), (+5) が生成され、それがリストに格納されます。

このような場合、文脈の値に同じ文脈にある関数を適用する処理が必要になります。型で表すと次のようになります。

f (a -> b) -> f a -> f b

Haskell では、このような処理を行う型クラス Applicative が用意されています。Applicative については次回で詳しく説明します。


初版 2013 年 3 月 2 日
改訂 2021 年 1 月 17 日