関数型言語のモナド (5)
モナド(概略)
関数型言語における「モナド」 とは、おおまかには以下のように考えて問題ありません。
関数型言語には bind 関数 という、ある種の操作を表す関数があります。(Haskell に bind という名前の関数があるわけではありません。)
データ型が bind 関数 の引数になれるとき、そのデータ型は モナド として利用できる、と言います。
まず、bind 関数とモナドの利用法の一例を見るために、Haskell にデフォルトで用意されている Maybe を利用してみましょう。
モナドの利用法 の一例を理解するだけでも大きな一歩となります。
Maybe の使い方
データ型 Maybe の定義は以下のとおりです。Maybe は、型変数 を1つ持っていることに注目してください。
data Maybe a =
Just a
| Nothing
型変数が1つ付加されているので、Maybe には1つだけ任意の情報を付加することができます。
どんな情報でも1つ持つことができるので、Maybe は コンテナ として利用されます。
Maybe はコンテナとして利用されることを想定しているため、値構築子 の Just という名前には特に意味がありません。
「何か1つの値を格納しているもの」を構築する値構築子として、「単なる入れ物」という意味合いで Just という名前が付けられています。
以下のように書くと、v1 は 5 という数値を格納していて、v2 は "abc" という文字列を格納している、という意味になります。
ghci> v1 = Just 5
ghci> v2 = Just "abc"
少しだけでも手を動かすと分かりやすいので、以下のプログラム Test2.hs をカレントディレクトリに作って保存してみてください。
-- Test2.hs
f x = case x of
Just a -> print a
Nothing -> print "Nothing..."
v1 = Just 5
v2 = Just "abc"
v3 = Nothing
上のプログラムを実行してみましょう。
ghci> :l Test2.hs
ghci> f v1
5
ghci> f v2
"abc"
ghci> f v3
"Nothing..."
上のプログラムを通して、Jsut や Nothing が 値構築子 であるという感覚を持ってもらえると良いな、と思っています。
(Nothing は、「値を格納していない」というデータ値です。)
Maybe モナド
Maybe の値は bind 関数 の引数になることができます。
そのため、データ型 Maybe は モナド として利用できると言えます。
Maybe をモナドとして利用する場合、その利用の仕方を Maybe モナド と呼んでいます。
まずは、Maybe をモナドとして利用するサンプルコード Test3.hs を作ってみましょう。
ユーザーが "70" のような文字列を入力してくれたら良いのですが、間違えて "abc" のような文字列を入力してしまうかもしれません。
このように、数値にならない文字列が入力されたらエラーとしたいところですが、それを実現する便利な関数があるため、まず、それを使ってみましょう。
-- Test3.hs
import Text.Read (readMaybe)
ga :: String -> Maybe Int
ga str = readMaybe str
文法解説は後回しにして、実際に関数 ga を使ってみましょう。
ghci> :l Test3.hs
ghci> ga "70"
Just 70
ghci> ga "abc"
Nothing
まず、Just 70 も Nothing も、Maybe Int 型の値 である、ということに留意してください。
次に、ga の動作は以下のようになったということを確認してください。
今後、「評価値」という言葉を使いますが、これは関数型言語で標準的に使われる表現で、関数の出力値のことです。 関数型言語では、関数に値を適用して評価値を得る という表現を標準的に利用します。
関数 ga に数値となる文字列を適用した場合、それを数値に変換し、Maybe コンテナに格納したものを評価値とする。
関数 ga に数値にならない文字列を適用した場合、Nothing を評価値とする。
次に、入力されたものが数値であったとして、その数値が 60未満であれば Nothing にしてしまいしましょう。
Test3.hs に、以下の関数 gb の1行を追加してください。
-- Test3.hs に追加
gb x = if x < 60 then Nothing else Just x
実際に、gb を使ってみましょう。
ghci> :l Test3.hs
ghci> gb 70
Just 70
ghci> gb 50
Nothing
さらに、入力された数値が、80 より大きいときも Nothing としましょう。そして、60以上 80以下の場合は、100倍した値を評価値としましょう。
Test3.hs に、以下の関数 gc の1行を追加してください。
-- Test3.hs に追加
gc x = if x > 80 then Nothing else Just (x * 100)
実際に、gc も使ってみましょう。
ghci> :l Test3.hs
ghci> gc 70
Just 7000
ghci> gc 90
Nothing
Maybe モナドを使ってみよう
Haskell における bind 関数は (>>=) という記号で表されます。
この関数は 2つの引数を受け取る関数 で、Maybe に対して利用する場合、「Maybe a 型の値」 と 「a -> Maybe b 型の関数」を受け取り、評価値は 「Maybe b 型の値」 となります。
入力と出力でコンテナ内容物の型が異なってもよいように、a と b の2つの型変数で表現されていますが、当然、a と b は同じ型でも構いません。
上の文章で、「Maybe に対して利用する場合」と断りを入れているのは、関数型言語では、1つの関数が 受け取ったデータ型ごとに異なる動作を行う ことが多いからです。
「Maybe に対して利用する場合」の bind 関数 (>>=) の動作は以下のようになります。
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
(>>=) x f = case x of
Just y -> f y
Nothing -> Nothing
(>>=) は、第1引数 x の形によって動作が変わります。
x が Just y の形であれば、評価値は第2引数の f を用いて f y とする。
このとき、f は a -> Maybe b 型であるため、f y の値は Maybe b 型の値になることに注目しておいてください。x が Nothing であれば、第2引数の f は無視して評価値を Nothing とする。
このとき、Nothing も Maybe b 型の値とみなせることに注目しておいてください。
Test3.hs を利用して、実験してみましょう。
ghci> :l Test3.hs
-- 関数 ga に "70" を適用してみます。
ghci> ga "70"
Just 70
--「直前に得られた値」と「関数 gb」を (>>=) に適用してみます。
-- it は直前に得られた値を指します。
ghci> (>>=) it gb
Just 70
-- さらに「直前に得られた値」と「関数 gc」を (>>=) に適用してみます。
ghci> (>>=) it gc
Just 7000
(>>=) の基本的な動作は、第1引数の中身 を取り出して、第2引数の f に適用する、となります。
慣れていないと分かりにくいかもですが、上の動作がしっかり納得できてから以下に進むようにしてください
関数 (>>=) は、一般的に 中置記法 を用いて利用します。
中置記法は、2つの引数 を取る関数に対して利用できる表現法で、
f (第1引数) (第2引数)
を、以下のようにして書く表現法です。
(第1引数) f (第2引数)
これを利用すると、さきほど実験した内容は、以下のように書くことができるようになります。
なお、中置記法を使う場合、(>>=) のかっこは省略できます。
ghci> ga "70" >>= gb >>= gc
Just 7000
実は、これが モナド の機能なのです。
関数を「うまい具合に」チェーンできるようにした仕組みが モナド です。
上の例で言うと、「左側から Maybe の値をもらって、処理をした結果の Maybe の値を右側に伝えていくチェーン」をどうやって構成するか、ということに対する1つの答えが Maybe モナド の利用なのです。
もう少しかっこ良く言うと、失敗の可能性がある context(前後関係、文脈)に対して Maybe モナドを利用する、と言います。
この記事の最初で言ったように、モナドの利用法は簡単です。
しかし、context のパターンによって利用の仕方が異なり、その応用範囲が広いのです。
さらに、「いろいろあるパターンに対して、どのように見れば統一的に扱えるのか」ということに関しては次章以降で見ていきましょう。
ところで、上の方で、関数を「うまい具合に」チェーンする、と言いましたが、その意味を確認してこの章を締めくくりましょう。
もう少しだけ Test3.hs を用いて実験してみます。
ghci> ga "abc"
Nothing
ghci> (>>=) it gb
Nothing
ghci> (>>=) it gc
Nothing
-- 上の動作をまとめると、以下のようになります。
ghci> ga "abc" >>= gb >>= gc
Nothing
-- 他にも少し試してみましょう。
ghci> ga "50" >>= gb >>= gc
Nothing
ghci> ga "90" >>= gb >>= gc
Nothing
次のように、ga, gb, gc の動作をまとめた関数 h を作っておくと便利ですね。
ghci> h str = ga str >>= gb >>= gc
ghci> h "70"
Just 7000
ghci> h "abc"
Nothing
ghci> h "50"
Nothing
ghci> h "90"
Nothing
Test3.hs の文法補足
Test3.hs の gb、gc は一般的な if 文で分かりやすいと思いますので、それ以外について補足しておきます。
最初の1行の import 文は、Text.Read モジュール に用意されている関数 readMaybe を読み込むものです。
readMaybe は文字列を数値に変換する関数で、以下のような動作をします。
数値に変換できる場合は数値に変換し、それを Just x の形にしたものを評価値とする。
数値に変換できない場合は Nothing を評価値とする。
上のように動作は簡単なのですが、1つ注意すべきことがあります。
数値に変換する場合、それを「整数(Int)」に変換するのか、「浮動小数点数(Float)」に変換するのかを指定しなければならないことです。
指定の仕方はいろいろあるのですが、Test3.hs にあるように、String -> Maybe Int と指定した関数に 束縛 させることで、readMaybe に整数に変換するように指示を出すのが簡単だと思います。