2008年10月28日火曜日

Haskell の fmap

A Gentle Introduction to Haskell: Classes によると、

fmap 関数は以前につかった map 関数を一般化したものです。

map と言えば、リストの各要素に対して関数を適用するための関数。例えば、リストの要素を 2 倍したいときは、

print $ map (* 2) $ [1,2,3,4,5]       -- [2,4,6,8,10]

map がリストのためにあるのに対し、fmap は自分で作成した代数的データ型に対して、map のように各要素に関数を適用するためにあるようだ。今回、以前に定義したシンプルな Iremono 型 を少し変更し、これに適用できるようにしてみる。

Iremono a 型の定義を以下のように変更。

data Iremono a = Iremono [a] deriving (Show,Eq)

前回は Iremono 型のフィールドが 型変数 a だったけれど、今回はリスト [a] にした。

 

fmap を使わずに

まずは、fmap を使わずに「入れ物」(Iremono a) の中の中身の要素を 2 倍にする関数を定義してみた。

と、その前に念のため確認。例えば次のように Iremono a 型に map 関数を適用してみても、

print $ map (*2) $ Iremono [1,2,3,4,5]

当然ながら怒られる。 (+_+)

    Couldn't match expected type `[a]'
           against inferred type `Iremono t'
    In the second argument of `($)', namely
        `Iremono [1, 2, 3, 4, ....]'
    In the second argument of `($)', namely
        `map ((*2)) $ Iremono [1, 2, 3, 4, ....]'
    In the expression: print $ map ((*2)) $ Iremono [1, 2, 3, 4, ....]

 

入れ物の中の要素を 2倍にする

Iremono の要素を 2 倍にする doubleIremono 関数を定義。

doubleIremono :: Num a => Iremono a -> Iremono a
doubleIremono (Iremono xs) = Iremono (map (*2) xs)

doubleIremono 関数を使ってみる。

print $ doubleIremono $ Iremono [1,2,3,4,5]  -- Iremono [2,4,6,8,10]

 

入れ物の中身の要素に関数を適用

doubleIremono 関数は「要素を 2 倍する」しかできないので、もう少し汎用的にしたい。そこで、関数を渡したら「入れ物」の中身に関数を適用する mapIremono 関数を定義。

mapIremono :: (a -> b) -> Iremono a -> Iremono b
mapIremono f (Iremono xs) = Iremono (map f xs)

これを使ってみると、例えば、

  print $ mapIremono (* 2) $ Iremono [1,2,3,4,5]
  print $ mapIremono (\x -> if x < 3 then "hoge" else "piyo") $ Iremono [1,2,3,4,5]

結果は、

Iremono [2,4,6,8,10]
Iremono ["hoge","hoge","piyo","piyo","piyo"]

 

A Gentle Introduction to Haskell の例を見る

最初に、Functor クラスの fmap 宣言を確認しておく。

fmap は、Functor クラスのメソッド。

class Functor f where
    fmap :: (a -> b) -> f a -> f b

A Gentle Introduction to Haskell: Classes では Tree a 型を Functor のインスタンスにする例が書かれている。Tree a 型は、

data Tree a    = Leaf a | Branch (Tree a) (Tree a)

(A Gentle Introduction to Haskell: Modules より)

これを以下のように Functor クラスのインスタンスにしている。

instance Functor Tree where
  fmap f (Leaf x)       = Leaf   (f x)
  fmap f (Branch t1 t2) = Branch (fmap f t1) (fmap f t2)

実装を見ると、各 Tree の要素に fmap 関数の第一引数である `f’ を適用しているのがわかる。

ここでいつも頭が混乱するのでゆっくり考える。(+_+)  上記のように型コンストラクタ Tree は Functor クラスのインスタンスであると宣言した。 Functor Tree の fmap の実装で返しているは Tree a 型。Functor クラスの定義の `f’ を Tree で置き換えることができるとすると、 次のよう想像することができる。

class Functor Tree where
    fmap :: (a -> b) -> Tree a -> Tree b

この fmap の型が Tree  a 型における実際の fmap の宣言ということ。だから、Tree  a 型の fmap が返している型が Tree b 型で OK と。

あ~ (@_@;) なかなか慣れない…。

 

Iremono a 型にも fmap

上記を真似して、fmap が適用されたとき、「入れ物」の中身である各要素に関数が適用されるように実装してみる。

instance Functor Iremono where
    fmap f (Iremono xs) = Iremono (map f xs) 

Tree のときと同じように Functor クラスの定義の f を Iremono で置き換えて考えてみると、

class Functor Iremono where
    fmap :: (a -> b) -> Iremono a -> Iremono b

上記で定義した fmap 関数の定義は、この宣言に沿うように定義されていることがわかる。

さて、実際に fmap を使ってみると、

  print $ fmap (*2) $ Iremono [1,2,3,4,5]   -- Iremono [2,4,6,8,10]

 

ところで、class Functor f where によると、

The Functor class is used for types that can be mapped over. Instances of Functor should satisfy the following laws:

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

上記を満たすように実装する必要があるらしい。第3回 mapからモナドを理解する:ITpro によると、

idは恒等関数(identity functionもしくはidentity)の略であり,入力したデータをそのまま返す関数です。(.)演算子は関数合成(functional composition)です。(.)の定義は,(f . g) x = f (g x)です。

確かに、型クラスによる宣言だけでは、関数がどのように振舞えばいいのかを規定することはできない。定義の内容はプログラマの手にゆだねられている。

「自分で定義した代数的データ型のフィールドの各要素に関数を適用していく」ということが、上記を保証することでなぜ実現できるのかという一番肝心なところがわからないのだけれど ^^; 、とりあえず一例だけでも試してみることに。  QQQ

main = do
  print $ test1   # True
  print $ test2   # True

test1 = (fmap id $ Iremono [1,2,3,4,5]) == (id $ Iremono [1,2,3,4,5])
    where id = (\x -> x)
test2 = (fmap (f . g) $ Iremono [1,2,3,4,5]) == (fmap f . fmap g $ Iremono [1,2,3,4,5])
    where
      f = (* 2)
      g = (+ 100)