2008年11月12日水曜日

Haskell で関数をつなげること – モナドの前に

Monad クラスの (>>=) メソッド。 bind と読むそうで、「束ねる」「結びつける」「しばる」などの意味がある。

2III[名]([副])] …を(場所・物に)結びつける, 縛りつける((on/to, on ...))
bind a person to a chair
人をいすに縛りつける

6 …を(氷・セメントなどで)固める, 凝結させる;(…で)〈料理の材料を〉つなぐ((with ...))
bind the gravel with cement
砂利をセメントで固める
 
use two eggs to bind the ingredients
材料のつなぎとして卵を2個使う.

(Yahoo!辞書 – bind より)

bind の型宣言は、

(>>=) :: m a -> (a -> m b) -> m b

第1引数に「何かをモナドで包んだもの」を与え、第2引数に「何かをモナドで包む関数」を与えると、「何かがモナドで包まれて」返ってくる。第1引数と返り値の型が共に「モナドに包まれた『何か』」なので、返ってきたものを更に (>>=) の第1引数に与え、また、その結果を (>>=) の第1引数に与え、そのまた結果を… というように、延々と入力と出力をつなげて繰り返し適用することができる。この間を取り持つのが (>>=) 関数。

使われているところを見ると、例えば、Doing it with class には、

maternalGrandfather s = (return s) >>= mother >>= father

左から右へと関数をつなげる役割をしているのがわかる。そして、そのつなげた中で何かごにょごにょやっているようだ。

… とは言ったものの、イメージとしてとらえることができても、どこか実感がわかない。書きながら、何かわかったようなわからないような…。 ^^; そもそも関数を「つなげる」というのはどういうことだろう?

 

関数を繰り返し適用

例えば、数値を1つ与えると + 1 して返す addOne 関数があるとする。

addOne :: Num a => a -> a
addOne x = x + 1

単純に適用するだけなら、

  print $ addOne 0

返ってきた値に繰り返し addOne 関数を適用するなら、

  print $ addOne $ addOne $ addOne 0

出力された値を関数の入力に使う。ごく当り前で普通のこと。

 

(>>=) を真似して

では、Monad の (>>=) 関数の型宣言に見かけが似ている関数 addOnef を定義してみる。この関数は上記の addOnef 関数の結果に、関数 f を適用した結果を返すことにした。

addOnef :: Num a => a -> (a -> a) -> a
addOnef x f = f $ addOne x

Monad の (>>=) 関数の型宣言を再掲すると、

(>>=) :: m a -> (a -> m b) -> m b

返り値の型が第1引数に適用できる型で、第2引数が関数という形。

addOnef を使ってみる。第2引数には「数値を1つ与えると数値を返す関数」を与える。二項演算子を部分適用したセクションはそれに該当するので、

  print $ addOnef 0 (* 100)                   -- 100
  print $ addOnef (addOnef 0 (* 100)) (/ 10)  -- 10.1

中置記法を使うなら、(cf. Haskell のセクションと中置記法)

  print $ 0 `addOnef` (* 100)
  print $ 0 `addOnef` (* 100) `addOnef` (/ 10)

これで全体が (>>=) に似た雰囲気になってきた。

 

シンボルを使って二項演算子を定義

次に、より似た雰囲気にするため、addOnef という名前を二項演算子として使えるシンボルを使って定義しなおす。

(>>=) に近い感じを狙って、

(-->) :: Num a => a -> (a -> a) -> a
(-->) x f = f $ addOne x

これを使うと、

  print $ 0 --> (* 100)
  print $ 0 --> (* 100) --> (/ 10)

さっきよりもつながってる感じが出てきた。 ^^

 

map や filter をつなげる

「つなげる」と言えば、リストに対して、map や filter を次々に適用していくことも連想。例えば、「0 以上の自然数の無限リストの要素を 2 倍して、その中で 10 以上のものを先頭から 3つだけ得たい」場合、

  print $ take 3 $ filter (> 10) $ map (* 2) [0..]    -- [12,14,16]

これを上記の (-->) のような感じでつなぐことはできないか試してみる。

先ほどの (-->) 関数は、 Num クラスのインスタンスが対象だったけれど、今度はリストに変更。

(-->) :: [a] -> ([a] -> [a]) -> [a]
(-->) xs f = f xs

map, filter 関数は、第2引数にリストを与えるとリストを返す。そこで、それら関数の第1引数を部分適用した関数を引数として与えることができるようにした。

これを使ってみる。

  print $ [0..] --> take 3                                  -- [0,1,2]
  print $ [0..] --> map (* 2) --> filter (> 10) --> take 3  -- [12,14,16]
  print $ [0..] --> filter (> 10) --> map (* 2) --> take 3  -- [22,24,26]

何となく以下の Ruby のコードような雰囲気。

p (0..100).select{|e| e>10}.map{|e| e*2}[0..2]

次のようにも書くこともできる。(cf. 2.7 レイアウト)

  print $ [0..] 
            --> filter (> 10)
            --> map (* 2) 
            --> take 3

 

all, any もつなげてみる

折角なので「人」を表わす Person 型を定義し、Person 型のリストから条件に合うものを filter で抽出、map で変換してみる。

まずは、Person 型とそのリスト。

data Person = Person {
      name :: String,
      age  :: Int
    } deriving Show

persons = [Person "Tarou" 20, Person "Hanako" 15, Person "Jiro" 15]

これに対して、年齢が 20 歳より若い人を抽出し、年齢を 2 倍したいなら、

  print $ persons
            --> filter (\p -> age p < 20)
            --> map (\p -> Person (name p) (age p * 2))

結果は、

[Person {name = "Hanako", age = 30},Person {name = "Jiro", age = 30}]

 

ついでに all, any 関数も適用したい。しかし、all 関数の型は、

all :: (a -> Bool) -> [a] –> Bool

filter, map とは異なる。そこで、次のように (?->) 関数を定義。

(?->) :: [a] -> ([a] -> Bool) -> Bool
(?->) xs f = f xs

これを用いて、

  print $ persons ?-> all (\p -> age p < 20)    -- False
  print $ persons ?-> any (\p -> age p < 20)    -- True

先ほどの (—>) 関数と組み合わせると、例えば、「20歳より若く、名前の先頭の文字が `H’ ではじまる人がいるか」について調べるなら、

  print $ persons
            --> filter (\p -> age p < 20)
            ?-> any (\p -> (name p `head`) == 'H')    -- True

 

ただ単に関数を適用する方向を変えただけで特に意味はないけれど、これで何となく関数をつなげるということが実感できた。ただし、Monad のように包んだ中身を渡すということは全くしてないけれど。 ^^;