この記事は Wantedly Advent Calendar 25日目の記事です。
最終日です!気合いが入りますね!!
Introduction
今回は Haskell のモナドの話をしたいと思います。
Haskell を学び始めた時、誰もが一度は経験するのが「モナドって何だ?」という疑問です。「Haskell モナド」で検索してみても、圏論を絡めた小難しい説明ばかりが出てきて、よく分からない事が往々にしてあります。
ところが、実は「Haskell におけるモナド」を理解する為に、圏論のモナドを理解する必要はありません。何故なら、「Haskell においてモナドである」為に必要なのは、「たった2つのルールを満たす事」だけだからです!
この記事では、「モナドとは何か」を簡単に説明したいと思います!!
Haskell におけるモナドとは?
Haskell におけるモナドとは、誤解を恐れずに言えば「いわゆる flatmap
っぽいものが使える、リストっぽい型」を指します。この、
- リストっぽい型である
- flatmap っぽいものが使える
という2つが、先ほど述べた「Haskellにおけるモナド」の条件です。それぞれの条件について詳しく見てみましょう。
ミニコラム: flatmapとは
flatmap
というのは 「コレクション A とコレクションを返す関数 f を受け取って、Aの各要素に f を作用させて得られた結果をフラットなコレクションにして返してくれる」関数です。 言葉だけで説明してもよく分からない感じですが、コードを見たら理解出来ると思います。Ruby や Scala, Swift なんかで使われていて、以下の様な感じになります。
irb> [1, 2, 3, 4].flat_map { |e| [e, e * 10] }
=> [1, 10, 2, 20, 3, 30, 4, 40]
scala> List(1, 2, 3, 4).flatMap(x => List(x, x * 10))
res1: List[Int] = List(1, 10, 2, 20, 3, 30, 4, 40)
> [1, 2, 3, 4].flatMap { [$0, $0 * 10] }
[1, 10, 2, 20, 3, 30, 4, 40]
flatmap
を Ruby で雑に実装すると、以下の様な感じになります。 map
してから flat
するから、flat_map
という訳ですね。
irb> def my_flat_map(arr, &block)
irb* arr.map { |e| block.call(e) }.flatten
irb* end
=> :my_flat_map
irb my_flat_map([1, 2, 3, 4]) { |e| [e, e * 10] }
=> [1, 10, 2, 20, 3, 30, 4, 40]
Haskellにおけるモナドの条件1: 「リストの様に、1つの型引数を受け取ってつくられる型である」
Haskell におけるモナドとは、リスト型やMaybe型、IO型の様な「型」を意味します(より厳密には、Monad
とは型クラスであり、「Monad
という型クラスに List型や Maybe型, IO型が属する」 と表現します。型クラスについては後ほど説明します。)
これらの型に共通するのは、どれも「1つの型引数を受け取る」という事です。。。。と、また新しく「型引数」という言葉が出てきたので、一度その意味を説明したいと思います。
型引数とは
Haskell には、「型を引数として受け取ってつくられる型」が多数存在しており、その時に渡される型が「型引数」です。
例えば、リストを考えてみましょう。Haskell におけるリストは同じ型の要素だけを含むコンテナ型として定義されており、その型は要素の型をa
として[a]
で表されます。ここでは、任意のa
型に対する[a]
型を総称して、「リスト型」と呼ぶことにします。
Int
だけを要素として持つリスト[1, 2, 3, 4]
は[Int]
型ですし、文字型Char
の要素だけを持つリスト['a', 'b', 'c']
の型は[Char]
です。これらのリスト型は、[]
という「型を引数としてとるもの」に対して、Int
やChar
のような型を渡したものと捉える事ができます。そのため、リスト型は「1つの型を型引数として受け取ってつくられた型」となります。
ちなみに、[]
は「型コンストラクタ」と呼ばれます。型を引数として受け取って型をつくりだすので、コンストラクタというわけです。
モナドは型引数を1つだけ受け取ってつくられる型
リストは型引数を1つだけ受け取ってつくられる型でした。リスト以外のモナド型についても、その型がどういった構造なのかを見てみましょう。
Maybe型
Maybe型というのは、「値が存在するかどうか分からない時」に使われる型です。その型はMaybe a
と表され、a
が任意の型となります。a
という型引数を受け取るので、Maybe
は型コンストラクタとなります。ここでは、Maybe a
型を総称して、「Maybe型」と呼ぶことにします。
例えば、何かしら失敗する可能性のある計算を行いたい時、失敗した事を明示的に示すために Maybe型が使えます。
例として、「要素とリストを受け取って、要素がリスト中に現れる位置を返す」関数を考えてみましょう。 elemIndex 2 [1, 2, 3]
を評価した時、 1
を返す様な関数elemIndex
です。この関数の振る舞いを考えた時、問題になるのは「要素がリストの中に存在しなかった」場合です。この時、elemIndex
は何を返したら良いのでしょうか?
1つの答えとして、「-1
の様なリストのindexにはなりえない値を返す」という方法もあり得ます。しかし、これは「-1
は失敗した時の値だ」という前提知識を利用者に強いる事になります。また、似た様な関数なのにライブラリによって失敗の表現の仕方が違うと言った状況も起こり得ます。「失敗は失敗」として明示出来る方が良い場合は多いのです。
こういった状況で活躍するのがMaybe型です。実はHaskellの標準ライブラリにはまさに「リスト中の要素の位置を返す」関数としてData.List.elemIndex
が用意されており、その関数の返り値はMaybe Int
型となっています。
Maybe Int
型の値はJust (Int型の値)
またはNothing
です。Data.List.elemIndex
は要素の発見に成功した時にはその「位置」をJust
でくるんで(Just 位置
という値で)返し、失敗した時にはNothing
を返します。成功や失敗が返り値から自明であり、さらに成功の時には欲しかった情報がその値の中に含まれています。理想的な振る舞いですね。
ghci> Data.List.elemIndex 1 [1, 2, 3]
Just 0 -- 1はリストの先頭(位置0)で見つかるので Just 0 が返る。Maybe Int型。
ghci> Data.List.elemIndex 10 [1, 2, 3]
Nothing -- 10はリスト中に存在しないので Nothing が返る。これもMaybe Int型。
この様に、任意の型の値を成功時に含むために、Maybe型は1つの型引数a
を使ってMaybe a
型と表されます。ちなみに興味深いのはNothing
で、これは任意のMaybe a
型に属する値となっており、文脈によってその型は変わります。例えばMaybe Int
型が期待される場面でNothing
が返ってきたらNothing
の型はMaybe Int
ですし、Maybe Char
型が期待される場面ではMaybe Char
型として振る舞います。見た目は同じでも、実は違うものなのです。
余談: Just
, Nothing
などのデータコンストラクタについて
ちなみに余談ですが、慣れないとっつきにくいのがJust 3
の様な値の表現で、Just
に違和感を感じるかもしれませんがこれは立派な「値」です。Just
は値を受け取って値を作る役割を果たしており、「データコンストラクタ」と呼ばれます。作られた値の表現にJust
という文字が入ってくるのが特徴的ですね。
同じく、Nothing
もデータコンストラクタですが、これは引数を受け取らないデータコンストラクタとなっています。引数を受け取らないデータコンストラクタとしては、Bool
型のTrue
やFalse
があると聞くと、なんとなくイメージがつかめるのではないでしょうか。
IO型
もう1つモナド型としてよく知られているのが、IO型です。例によって、任意の型a
に対してIO a
型が存在し、これらのIO a
型を総称してIO型と呼びます。IO型は、「入出力を伴う操作」の型を表現するために使われます。
例えば、標準出力にHello, World!
と出力するHaskellプログラムを書きたいとしましょう。実はHaskellには標準出力へ文字列を出力する為に使われる関数print
が標準で用意されています。この関数は文字列化が可能な値
を受け取って、IO ()
型の値を返します。そしてそのIO ()
型の値が持つ「副作用」によって、標準出力へ文字列の出力がなされます。使い方イメージは以下の様な感じです。IO型の値であるからこそ、「標準出力への出力」というIO操作が可能となっています。
$ echo 'main = print "Hello, World"' > HelloWorld.hs
$ ghc --make HelloWorld.hs
$ ./HelloWorld
"Hello, World"
()
というのは「unit型」と呼ばれる型で、意味のある値が無い事を示す為に使われます。今回のケースでは「標準出力へ出力する」という副作用自体が重要であった為、IO ()
型が使われていました。
IO型の値の中でも、意味のある値を持ちたいケースは多々あります。例えば、標準入力から文字を入力として受け取ってプログラムの中で使いたい時、HaskellプログラムではgetChar
という関数が使われます。この関数は引数を受け取らず、IO Char
型の値として評価されます。ここでIO ()
型ではなくIO Char
型となっているのは、「標準入力から受け取った文字」がChar
型だからです。つまり、「標準入力から受け取った文字をIOでくるんだ値」を表現する為にIO Char
型が使われるのです。
IO型には他にもIO String
などが存在していて、IO
は1つの型を引数として受け取って型をつくり出す型コンストラクタとなっています。
条件1まとめ
ここまで見てきた様に、モナド型はどれも「型引数を1つだけ受け取ってつくられる型」となっています。つまり何かしらの型の値をくるんだ構造となっていて、この「くるまれた値をどう取り出すのか」という問題が次の条件2に繋がってきます。
Haskellにおけるモナドの条件2: 「他の言語でいうflatMapと同様の役割を果たす、>>=(バインド)という関数が定義されている」
モナド型の値に対しては、>>=
という関数が必ず定義されています。これは「バインド」と呼ばれる関数で、特にリストに対しては、他の言語でいう flatMap
と同じ動作をします。
例を見てみましょう。以下のコードでは、1から5までの数字のリストである list
と、値を受け取ってその2倍の値を2つ並べたリストを作る関数である makeDoublePair
を定義しています。そして >>=
を list
とmakeDoublePair
に対して適用させると、元のリストの各要素が2倍の値で2つずつ並んだリストが作られます。まさに flatMap の様な動作になっているのが分かるでしょうか?
Prelude> let list = [1, 2, 3, 4, 5]
Prelude> let makeDoublePair = \x -> [x * 2, x * 2]
Prelude> list >>= makeDoublePair
[2,2,4,4,6,6,8,8,10,10]
>>=
を理解する際、注目するべきなのはその「型」です。>>=
は +
などと同じ様に中置演算子(引数の間に書く関数)であり、list
と makeDoublePair
の2つを受け取って [2,2,4,4,6,6,8,8,10,10]
を返す関数となっています。つまり、 Num t => [t]
と Num t => t -> [t]
を受け取って、Num b => [b]
を返す関数となっています。これは、Haskell の REPL である gchi で :type
コマンドを打って実際に確認してみると分かりやすいかもしれません。(:type
はその後に書いた値の型を表示してくれるコマンドです。値の後に ::
を印字した後、型が表記されます。)
Prelude> :type list
list :: Num t => [t]
Prelude> :type makeDoublePair
makeDoublePair :: Num t => t -> [t]
Prelude> :type (list >>= makeDoublePair)
(list >>= makeDoublePair) :: Num b => [b]
Num t => [t]
型というのは、Num
型クラスに属する任意の型を「 t
」と読んだ時に、その t
のリスト型を表しています。「型クラス」というのは Haskell において型をグループ化する為の仕組みで、数字を表す型は Num
型クラスに属しています。Haskell では 1
や 2
のような数値リテラルだけではどの型になるかわからない(Int32, Int64, Integer, Float, Double など様々な型となりうる)為、型が確定するまでは Num型クラスに属する型のどれか
として扱われます。そして、:type
コマンドなどで型を印字する際は、Num 型の任意の型a
という意味で Num a
という表記を使い、それを =>
の左側に印字し、 =>
の右側に a
を用いて表現した型を記載します。(a
や b
, t
など型を表す変数としては様々なものが使われますが、意味は無いので気にしなくて良いです)
Prelude> :type 1
1 :: Num a => a
また、a -> b
という表記は 「a
型の値を受け取って b
型の値を返す関数の型」を表していて、例えば makeDoublePair
の型である Num t => t -> [t]
は 「Num
型クラスに属する t
型の値を受け取って、t
型のリストを返す関数の型」を表しています。
これらの表記を用いて >>=
の型を記載すると、 Num t => [t]
と Num t => t -> [t]
を受け取って、Num b => [b]
を返す関数であることから Num t => [t] -> (t -> [t]) -> [t]
型である事がわかります。(Haskell では、a
型と b
型の値を受け取って c
型の値を返す関数の型を a -> b -> c
と表現します。これは厳密には 「a
型の値を受け取って b -> c
型の関数を返す関数の型」なのですが、2つの値を関数に適用するという操作が「1つの値を適用した返り値の関数にもう1つの値を適用する」操作と同一視出来るため、Haskell ではこのような表記を行います。なお、このような「複数引数関数を単一引数関数をネストしたものとみなす」操作は、カリー化と呼ばれます)
では、実際に ghci で >>=
の型を調べてみましょう。
Prelude> :type (>>=)
(>>=) :: Monad m => m a -> (a -> m b) -> m b
Monad
というキーワードが出てきました。これは、Monad
という型クラスに属する m a
や m b
型が使われる関数という事を意味しています。
ここで、「Haskellにおけるモナドの条件1」の「モナド型は型引数を1つだけ受け取ってつくられる型」という文章を思い出してみましょう。モナド型は、[]
や IO
の様な型コンストラクタを持ち、1つの型を引数として受け取る事でつくられます。>>=
の型に出てきた Monad m
の m
はこういった型コンストラクタを表しています。
>>=
の型である Monad m => m a -> (a -> m b) -> m b
という表記は多少面喰らうかもしれませんが、m
をリストの型コンストラクタである []
で、 a
と b
を Num t => t
で置き換えると、まさに Num t => [t] -> (t -> [t]) -> [t]
となっている事がわかると思います。
リスト型に対して >>=
を適用した際にも、確かに Monad m => m a -> (a -> m b) -> m b
という型になっている事がわかりました。
Monad 型クラス
Haskell におけるモナド型とは、 「Monad
型クラスに属する型」の事を指します。つまり、Haskell においてモナド型が欲しければ、 Monad
型クラスに属する為の条件を満たせば良い事になります。
では、Monad
型クラスに属する為の条件とはなんでしょうか?それこそがまさに、>>=
という関数が定義されている事(それに加えて、return
という関数が定義されている事)なのです。
「型クラス」は Haskell における型のグループ化の仕組みと述べましたが、グループに属する条件は「指定された特定の関数を定義している事」になります。例えば、順序付け可能な型が属する Ord
という型クラスは、以下の順序付けに使われる関数の定義を要請します。<
や <=
, >
や >=
の定義が必要な点が、特徴的です。
compare :: a -> a -> Ordering
(<) :: a -> a -> Bool
(<=) :: a -> a -> Bool
(>) :: a -> a -> Bool
(>=) :: a -> a -> Bool
max :: a -> a -> a
min :: a -> a -> a
Monad 型クラスを定義する Haskell コードは以下のようになっていて、Monad 型クラスにおいては、この「定義されている必要のある指定された関数」が >>=
と return
になります。
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a
>>=
はリスト型の例でも見たように、モナド型の値 m a
と、モナド型の値を返す関数 a -> m b
を受け取って、モナド型の値 m b
を返す関数です。その定義の中には、Haskellにおけるモナドの条件1の 「m
が1引数の型コンストラクタである」という条件が入ってきています。また、この関数を定義することがHaskellにおけるモナドの条件2そのものでした。
return
はいわゆるデータコンストラクタで、任意の型の値を受け取って、モナド型の値を作り出します。例えば、リスト型のコンテキストで return 3
を評価すれば [3]
になりますし、 Maybe 型のコンテキストで return 3
を評価すれば Just 3
になります。本当は return
を定義する事も「Haskellにおけるモナドの条件」に加えるべきなのですが、1引数のデータコンストラクタの定義は大抵サクッと出来ちゃうので省略していました。
また、より厳密に言うと現在は Functor
や Applicative
型クラスに属する事も Monad
型クラスに属する為の必要条件となっている(なおかつ Minimal complete definition は >>=
の定義だけになっている)のですが、 https://wiki.haskell.org/Functor-Applicative-Monad_Proposal を見ると分かるように、 >>=
と return
の定義さえ存在していれば、お決まりのコードを書くだけで Functor
や Applicative
型クラスの条件を満たす事が出来ます。
ここまでで、「Haskellにおけるモナドの条件1, 2」を満たせば Monad
型クラスに属する事ができる、つまりHaskellにおける「モナド型」となれる事が分かりました。
モナド型の便利さ
Haskell においてモナド型である事、つまり Monad
型クラスである事は、>>=
が定義されている事とほぼ同義である事が分かりました。では、なぜ「モナド型」がこれほどまでに持て囃されるのでしょうか?>>=
がそれほど偉いのでしょうか?
実は、>>=
の凄さはその「便利さ」にあります。例として、Maybe
型における >>=
の利用を考えてみましょう。Maybe
型は、計算が成功するか失敗するかが分からない時に返り値を表現する為に使われる型です。失敗の可能性のある計算を連続して行いたい時に、>>=
は便利な記法となります。
以下のコードでは、文字列から数字の読み取りを Text.Read.readMaybe
で行い、その計算が成功した時にだけ、得られた数字を使って Data.List.elemIndex
を評価しています。Text.Read.readMaybe
は文字列からの読み取りに成功すれば Just
で包んだ値を、失敗すれば Nothing
を返す関数であり、Data.List.elemIndex
は要素とリストを受け取って「要素が何番目に現れたか」を Just
で包んで返す関数です。どちらも、引数によっては計算に失敗する(Nothing
を返す可能性のある)関数です。
*Main> let list = [1, 2, 3, 4, 5]
*Main> let string = "2"
*Main> (Text.Read.readMaybe string) :: Maybe Int
Just 2
*Main> (Text.Read.readMaybe string) >>= (\x -> Data.List.elemIndex x list)
Just 1
>>=
を使ってこういった計算を繋げると、失敗しない限りはそのまま計算を続けてくれて(Just でくるまれた値を取り出して使ってくれて)、失敗した瞬間に全体の計算結果を Nothing
にしてくれます。この際、計算と計算の間でわざわざ Nothing
かどうか確認するためのコードを記述していないのがポイントです。その役割は >>=
が担ってくれる為、コードはロジックの記述に集中したシンプルなものになります。
実際、Maybe型における >>=
の定義は以下のようなものになっていて、Nothing
かどうかの判定は >>=
が行ってくれています。
(Juxt x) >>= f = f x
Nothing >>= f = Nothing
IO 型でも同様で、例えば外部から読み取った値を使って計算を行う場合に >>=
を使います。例として、以下のコードを考えてみましょう。
main = getLine >>= (\name -> putStrLn ("Hello, " ++ name))
getLine
を評価した値の型は IO String
で、外部からの標準入力を IO
でくるんだ値となっています。ここから値を取り出して計算を行うために、 >>=
が使われます。
いくつもの入力を読み込むことももちろん可能で、その為には >>=
をネストさせます。
main = getLine >>= (\firstName -> (getLine >>= (\lastName -> putStrLn ("Hello, " ++ firstName ++ lastName))))
これで、firstName
と lastName
の2回分読み込みを行うことができます。こうやって簡単に IO の数を増やしていけるのも、>>=
の便利ポイントです。
ちなみに、>>=
の数が増えるとどんどんコードが読みづらくなっていくので、それを緩和する為に do構文
と呼ばれる記法がよく使われまます。先ほどのコードを do構文
を使って書き直すと以下のようになります。
main = do
firstName <- getLine
lastName <- getLine
putStrLn ("Hello, " ++ firstName ++ lastName)
格段に読みやすくなりました。この様に do構文
を使えるのも、モナド型の大きな特徴です。
モナド則って何?
最後に、モナド則についても触れておきたいと思います。モナド則とは、Monad
型クラスとして定義したモナド型が「圏論の意味でのモナド」となる為に満たすべきルールであり、また実際に扱いやすい(想定通りの振る舞いをする)型である為に満たしておいて欲しいルールでもあります。
モナド則は以下の3つの等式で表現できます。
Left identity: return a >>= f と f a が等価
Right identity: m >>= return と m が等価
Associativity: (m >>= f) >>= g と m >>= (\x -> f x >>= g) が等価
これらの意味について興味が湧いた方は Haskell Wiki の Monad laws を読んでみてください。ここではざっくりと、モナド則を満たさない場合になぜ扱いづらいモナド型となってしまうかを例を通して見てみます。
>>=
は Monad m => m a -> (a -> m b) -> m b
という型になっていれば良い為、様々な定義がありえます。例えば、Maybe
型によく似た MyMaybe
型を考えてみます。MyMaybe
型の値には MyNothing
と MyJust a
が存在し、Maybe
型とよく似た動作をするものの、>>=
が常に MyNothing
を返すとします。
instance Monad MyMaybe where
return a = MyJust a
_ >>= _ = MyNothing
>>=
の型はちゃんと Monad m => m a -> (a -> m b) -> m b
となっている為、MyMaybe
型は Monad
型クラスに属しています。
ただし、Monad 則は満たしていません。試しにRight identity を考えてみると、任意のMyJust a
や MyNothing
となりうる m
に対して m >>= return
は常に MyNothing
となる為、Right identity は成り立っていない事が分かります。
そして、実際に MyMaybe
型における >>=
は使い物になりません。常に MyNothing
を返す為、意味のある計算を行う事が出来ないからです。モナド則をちゃんと満たすことで、こう言った状況は避ける事ができます。
まとめ
Haskell におけるモナドとは、「Monad
型クラスに属する型」の事で、その条件は「1つの型引数を持つ型コンストラクタ」を持ち、「return
と >>=
が定義されている」事です。また、モナド則と呼ばれるルールを満たす事で、圏論の意味でも正しい「モナド」となり、実用上も扱いやすい型となります。
なにやら小難しく思える「モナド」ですが、これらの簡単なルールを満たすだけでモナドになってしまいます。難しく考えず、モナドと戯れてみましょう!
皆で楽しく、Let's Haskell!!