Haskell

Haskell,モナドの自分なりの理解

More than 3 years have passed since last update.

僕はしがないjavaプログラマだが、個人でscalaとかを触っている。

scalaはFP(関数型プログラミング)のパラダイムも持っていて、FPのことも調べだすとHaskell, そしてモナドが登場して、良くわからなくなってくる。

モナドは像とか言われても、お前は何を言っているんだ状態なわけで、

すごいHaskellたのしく学ぼう!に手を出した。

結局のところ


  • いきなりモナドとは何かを学ぶより、基礎から学ぶ

  • コードを書いて体感してこそ、モナドの便利さが実感できる

という事がわかった。(ポリモフィズムやデザインパターンの便利さをOOPやった事ない人に説明しろと言っても、わかってもらえないのと同じ)

本を読んで自分なりに理解した事なんぞをメモとして残しておく。


正確な理解とは程遠い部分もあるかもしれないが、そこはご容赦を。


Haskellでは型が大事だ(Javaよりも、型が多くの事を教えてくれる)。


色々な形式の宣言が可能だ。


宣言

data 型名 = 値コンストラクタ

例)

data Signal = Red | Yellow | Green

data Point = Point Int Int
data Person = Name{firstName :: String, lastName :: String}
data Shape = Rectangle Point Point | Circle Int Point

信号(signal)のようなenum的なもの(列挙型)、PersonのようなJavaのオブジェクト的なフィールド付き型、 図形(Shape)のような四角または円のどちらかを取るというようなJavaでは継承を用いないとできない定義が定義可能だ。

最後の形式(あるいは、全部ひっくるめて?)を、代数的データ型(Algebraic Data Type)と呼ぶ。


型コンストラクタ

上記で値コンストラクタに引数を取れるものがあったが、実は型名部分にも型を引数として取れるものがある。

例)

data Maybe a = Nothing | Just a

aが型引数で、 Javaのジェネリクス(List<T> のT)みたいな物。

Maybeは型ではなく、型引数をとって型を生み出すもの(Maybe Intとか、Maybe Stringとかが、Maybeから作られた型)で型コンストラクタという。

型コンストラクタは型の世界の関数みたいな扱い(Kindと呼ぶらしい)になる。

また、以下のような再帰的なデータ型も定義可能だ。

data Tree a = Empty | Node a (Tree a) (Tree a) 

型コンストラクタ(Maybe, Listなど)は、普通の型を包んで何かするようなものに使われ、

型と型コンストラクタの区別を付けておくことが大事。


型クラス

そして、型クラス。Javaからすると、意味が全く違うので注意が必要。


型クラスは関数の定義をまとめるもので、Javaのインターフェースみたいなもの。

別に型を引数や戻り値にする関数は好きに書ける。

fullName :: Person -> String

fullName p = firstName p ++ " " ++ lastName p

が、型をある型クラスのインスタンスとし(Javaでいうインターフェースの実装。インスタンスの意味も全く異なる)、型クラスの関数を(必要であれば)実装する事で、自作の型に様々な機能を追加できる。


型クラスの制約をつけることで、関数を実行できる型を限定させると言い換えても良い。


  • 型クラス Eq のインスタンスとすると、==などの一致関数を自作の型で適用できる。

  • 型クラスOrdのインスタンスとすると、 自作の型の大小比較やソートができる。

型クラスEqはこんな感じ

class Eq a where 

(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool

==のように、特定の型クラスのインスタンスでないと使えない関数は関数の定義に、

(Eq a) => a -> a -> Bool

のように、型クラスの制約が付く。

自作の型をEqのインスタンスとするにはこんな感じ

instance Eq Person where 

  a == b = firstName a == firstName b && lastName a == lastName b

これで、型Personは型クラスEqの持つ機能を使えるようになった。


ちなみに、Eqのような良く使う型クラスは上記のようにしなくても、deriving で自動導出可能だ。

Javaと違って、型の宣言と関数の実装は別々に行ってよく、関数を後付可能なのは非常に便利だ。

また、型クラスOrdは大小比較を行う前提として、まず同一性の判定ができること(つまり、Eqのインスタンスである事)を求めている。型クラスに前提となる型クラスがある場合は、

class (Eq a) => Ord a のように制約が付く。これはJavaだとインターフェースの継承のようなもの。

とりあえず、型に関するこれらの前提がないと、この後のFunctor,Applicative,Monadoを理解するのは難しい。


Functor

ファンクターは型コンストラクタに対する型クラスで、型コンストラクタで包まれた値を、型コンストラクタに包まれたまま写す。

定義はこんな感じ

class Functor f where

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

fは f a, f b となっている事から型変数を取る型、つまり型コンストラクタである。

add3 i = i + 3 のような加算する関数は、もちろん数値に対して動作するが、Just 2[1,2,3] のような型コンストラクタに包まれている値には適用できない。

そこで、ファンクターの fmapを使う。

add3 i = i + 3

add3 4 --7
fmap add3 $ Just 4 -- Just 7
fmap add3 $ [4,5,6] -- [7,8,9]

以上のように普通の関数が、fmapによって、包まれた値に対して作用可能になった。

(a -> b) -> f a -> f b(a -> b) -> (f a -> f b)とみると、通常の関数a -> bが、型コンストラクタfに対する関数に底上げされているように見える(liftとも言うらしい)。

Functor、Applicatice,Monadは結局のところ、型コンストラクタに対して共通的な操作を提供する手段だと、私は理解した。

自作の型コンストラクタについても、Functorのインスタンスとすれば、fmap が利用可能となる。


Applicative

アプリカティブは、複数の引数を取る関数を型コンストラクタに適用させるもの。

代表的な定義は以下の通り、

class Functor f => Applicative f where

pure :: a -> f a
(<*>) :: f(a -> b) -> f a -> f b

fmapと似ているが異なり、関数そのものを型コンストラクタに包む。

(pure は 関数を渡して f(a -> b) を作るのに主に使う。)

2つのMaybe値、 Just 3Just 4を加算するにはどうすればよいか?

fmapでは無理であり、アプリカティブの<*> と2引数を取る通常の関数+を組み合わせる事で可能となる。

(+) 3 4 -- 7

pure (+) <*> Just 3 <*> Just 4 -- Just 7
(+) <$> Just 3 <*> Just 4 -- 上と同じ

また、関数の引数が3つ以上なら、それだけ <*>による連結を増やせばよく、アプリカティブスタイルと呼ぶ。


Monad

んで、ようやくモナド。

ファンクターは1引数関数、アプリカティブは複数引数の関数を型コンストラクタで包まれた値に適用する手段だった。

モナドは、関数合成を、型コンストラクタについて適用する手段だ。

主な関数はreturn, >>=の2つ。

 return :: a -> m a

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

前述のadd3は関数合成可能だ。

add3 . add3 . add3 . add3 $ 4 -- 16

add3(add3(add3(add3 4))) -- 16

ところが、引数が0の場合に計算を失敗させるとなると、戻り値はMaybe Int となり、

前述の合成はできなくなる。

addM3 0 = Nothing

addM3 p = Just(p + 3)

モナドの、 >>=を使うと可能になる。

Just 4 >>= addM3 >>= addM3 >>= addM3 >>= addM3 --Just 16

失敗パターンでも利用可能だ。

Just (-6) >>= addM3 >>= addM3 >>= addM3 >>= addM3 --Nothing

引数の場所は異なるが、>==を.と考えると、大体前述の関数合成と似た書き方ができる。

また、do記法の表現もサポートされる。

do a1 <- return (-6)

a2 <- addM3 a1
a3 <- addM3 a2
addM3 a3

a -> m a のような型コンストラクタを戻り値とするような関数に

fmapを使うと、 Just(Just,,)のように型コンストラクタの入れ子になってしまう。

ここで、>>=を使うと、型コンストラクタの性質を保ったまま、元の値に関数を適用可能となる。


まとめ


  • ファンクター、アプリカティブ、モナドは型を取る型(型コンストラクタ)に対して、汎用的な機能を提供する手段。どんな処理をするかはその型次第。(要はリストにある便利関数を色んな型で使えるようにしようという事だと思う)


    • ファンクター,アプリカティブ,モナドの順で高機能になっていく。

    • アプリカティブで充分なケースも多いだろう。



  • モナドは定義だけみれば、ふーんという感じだが、モナドは様々な型に用意されているので、色々コード書くと便利さが見えてくる(はず)


    • 私はまだ実感できてない。