Haskellの理解度がペラッペラなので勉強し直した、そんなまとめ
モナドとか軽々に言うと怖いお兄さんが嬉々としてマサカリ投げに来るらしい
あーもなどもなど
きっかけと目的
使ってみたいモジュールがあるのと、ちゃんと理解したいコードがあるから
なので目的はとりあえず読める様になり、あり物を使える様になること
知りたかったことと勉強前理解度
キーワード | 理解 |
---|---|
instance | 自前でShowを書くときに使うやつ |
Functor | なんかmapできるらしい、はて |
ApplicativeFunctor | Functorよりすごいらしい、恐い |
Monad | ApplicativeFunctorよりすごいらしい、怖い |
pure | よくみる |
fmap | たまにみる |
<$>と<*> | よくみる |
do | 超よくみる、全然わからん |
やってないこと
- 自作はまだしないので、則について細かく理解するのはまた今度
- Functor則
- ApplicativeFunctor則
- Monad則
- 直近で使いたいMaybe, Either以外のモナドはまた今度
- IO
- List
- Writer
- Reader
- State
教材
すごいH本
さっそく、の前に
Maybe
が一番わかりやすいと思い、H本の展開に則って自分で作ってみることにした
data Opt a = None | Some a
なんとなくScala風
ずっとこいつを使います
あと、今後はOptとかMaybeとかIOとかの型コンストラクタを文脈と言います
Opt Int
はOptの文脈に入ったInt値
、みたいな
derivingとinstance
まず間違いなく自前の型をFunctorとかにするにはinstanceキーワードが出てくるはず
Showで練習しておかねば
deriving
よく使う感じ
Haskellにお任せする
data Opt a = None | Some a deriving Show
instance
自分で挙動を変えるなら、導出(deriving)しないでinstanceキーワードで自分で定義する
instance Show (Opt a) where
show None = "None"
show (Some a) = "Some: " ++ show a
あれ、これだとshow a
が出来る保証がないな...どっかにShow a =>
って書かないといけないのかな?
instance (Show a) => Show (Opt a) where
show None = "None"
show (Some a) = "Some: " ++ show a
こんな感じか
Functor
関数を、文脈のある値に適用できる
実装
こんな実装らしい
class Functor f where
fmap :: (a -> b) -> f a -> f b
中身の値を直接変換する関数と、Functorの値で、新たなFunctorの値にする、って感じっぽい
Optで考えると、中身があるなら適用して、ないならないまま、って感じかな?
さっそくMaybeの実装を写経する
instance Functor Opt where
fmap f None = None
fmap f (Some a) = Some $ f a
Functor f
とFunctor Opt
が対応しているので、さっきのfmapを読み替えてみる
fmap :: (a -> b) -> Opt a -> Opt b
具体化すると簡単に読めるな
使ってみよう
fmap (+2) None
-- None
fmap (+2) (Some 5)
-- Some: 7
中身があるかどうか気にしないで扱えちゃうってことだね、便利
使う
もう少しいろいろやってみよう
適当な2つの関数を作っておいて、いろいろ合わせ技をしてみる
optHalf :: Int -> Opt Int
optHalf x = case even x of
True -> Some $ half x
_ -> None
half :: Int -> Int
half = (`div` 2)
let some = optHalf 40
some
-- Some: 20
fmap optHalf some
-- Some: Some: 10
-- 間違えた、これは (Int -> Opt Int) を Opt Int の中身に適用しちゃうのでネストしちゃう
fmap half some
-- Some: 10
-- もう一度半分にしたい場合は?
fmap half (fmap half some)
-- Some: 5
-- 演算子化して間に置いてみる
half `fmap` some
-- Some: 10
-- ところで <$> ってのが fmap と同じらしい
half <$> some
-- Some: 10
-- 中身に2度適用するなら、先に関数合成して置いても同じ なるほど
fmap (half . half) some
-- Some: 5
なんとなく好きに書ける様になったぞ?
まとめ
class Functor f where
fmap :: (a -> b) -> f a -> f b
- 1引数関数を中身に適用する
-
<$>
はfmapと同じ - 関数もファンクターに含まれる場合、ファンクター値を写すことは出来ない
疑問
ところで、Some 5 + Some 3
とかは出来ないのかな?
あとhalf
は奇数でも割っちゃうので、optHalf
を繰り返したい
ApplicativeFunctor
文脈のある関数を、文脈のある値に適用できる
Functorの出来ないこと
2引数関数だとどうなるか
ghci> :t fmap (+) (Just 3)
fmap (+) (Just 3) :: Num a => Maybe (a -> a)
関数がMaybeに入ってしまった
- 関数もファンクターに含まれる場合、ファンクター値を写すことは出来ない
fmapではこれ以上どうにもならないっぽい
実装
Applicativeの実装はこんな感じらしい
class (Functor f) => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
- ApplicativeならFunctorであるという制約がついている
- pureはデフォルトの文脈に突っ込む最低限の処理らしい、はて?
-
(<*>)
はfmapと似てるけど、最初の関数も文脈に含まれている
OptもApplicativeFunctorにしてみよう
instance Applicative Opt where
pure = Some
None <*> _ = None
(Some f) <*> something = fmap f something
pure = Some
か、なるほど、Optにするってことか
次の行は、f (a -> b)
がNoneだった場合は計算しようが無いのでNoneになると書いてある
最後の行はf (a -> b)
がなんかだった場合、(a -> b)
の部分をfmapすると書いてある
最後の行がポイントっぽい
まるでf (a -> b)
からfを外しているみたい
それにsomethingにfmapしたらやっぱり結果はOptになるのと、Functorであることが制約で保証されてるからfmapが使えるってのがキモらしい
使う
まず、fmapで2引数関数を取るのと、1引数関数を文脈に入れるのは同じ
ghci> :t fmap (+) (Some 3)
fmap (+) (Some 3) :: Num a => Opt (a -> a)
ghci> :t Some (+3)
Some (+3) :: Num a => Opt (a -> a)
文脈に入れるのはpureも同じ(実際pure = Some
だし)
ghci> :t pure (+3)
pure (+3) :: (Num a, Applicative f) => f (a -> a)
けどpureには「Optの文脈だよ」って言ってないから、まだ抽象的な持ち方をしているって感じかな?
この3種どれでも、<*>
で同じ文脈の値に適用できるんだったね
fmap (+) (Some 3) <*> (Some 5)
-- Some: 8
Some (+3) <*> (Some 5)
-- Some: 8
pure (+3) <*> (Some 5)
-- Some: 8
おぉ, Opt Int に Opt (Int -> Int) が適用できてる
もちろんどちらかが None ならお察しの通り
None <*> (Some 5)
-- None
pure (+3) <*> None
-- None
もう少し触ってみよう
fmapは<$>
とも書けるから
(+) <$> (Some 3) <*> (Some 5)
-- Some: 8
おぉ!よく見るやつだ!最初だけ<$>
でそれ以降が<*>
なやつ!
3引数も...試してみよう!
let foo x y z = x + y + z
foo 1 2 3
-- 6
foo <$> (Some 1) <*> (Some 2) <*> (Some 3)
-- Some: 6
foo <$> (Some 1) <*> None <*> (Some 3)
-- None
おぉぉぉぉ!楽しい!
ところで、
Some 5 + Some 3
とかは出来ないのかな?
できたね!
これすごいのはfooはあくまでInt -> Int -> Int -> Int
なのに、Optの文脈で使えちゃってる点
foo自体はそのまま使っていて、違う文脈でも使えちゃうのがすごいところ
だから例えばEitherでもおk
foo <$> (Right 1) <*> (Right 2) <*> (Right 3)
-- Right 6
foo <$> (Right 1) <*> (Left "even not allowd") <*> (Right 3)
-- Left "even not allowd"
カッチョイイZE!
まとめ
class (Functor f) => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
- 文脈に入った関数を文脈の値に適用する
- 引数はいくつでも良い
-
<*>
を使って適用する
以下の結果は全部同じ
fmap (+) (Some 1) <*> (Some 2)
(+) <$> (Some 1) <*> (Some 2)
pure (+) <*> (Some 1) <*> (Some 2)
疑問
この時点で結構凄いと思う
Monadってのは何が出来るん?
Monad
文脈のある値を、文脈を付けて返す関数に適用する
文脈を保ったまま、関数に渡したい
って感じ?
実装
実装を見よう
class Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
x >> y = x >>= \_ -> y
fail :: String -> m a
fail msg = error msg
- returnはpureみたいなもの
-
(>>=)
は良く聞くbindと言うやつ -
(>>)
はデフォルト実装がある、普通は上書きしないらしい -
fail
もデフォルト実装がある、普通は自前で呼ばないでHaskellが呼ぶらしい
returnは文脈につっこむだけなんだね!(正直なところ、IO専用のイメージだった)
大抵最後は文脈に突っ込んで終わるから、最後の行がreturnって場合が多いのかな
では最後にOptにMonadになってもらおう
instance Monad Opt where
return = Some
None >>= f = None
Some x >>= f = f x
fail _ = None
returnはpureと同じで、Optの世界に値を放り込むんだね
None >>= f
がNoneなのはイメージしやすい
Some x >>= f = f x
って、まるでxからSomeを引っぺがしている様だね、けど(a -> m b)
だから、f x
はまたSomeの文脈に戻る
これが文脈を引っぺがすみたいだけど、文脈からそとに漏れないってことなのかな
failはただのNoneになったみたい、失敗系モナドだから、例外よりその方が良さげ
使う
まずreturnから
returnは別にIO専用ではない!
return 5 :: Opt Int
-- Some: 5
return 5 :: Either String Int
-- Right 5
ちゃんとなんかの文脈に入って返ってきた
次は>>=
ね、いままで何となくで書いてなんか怒られてたこれ、今見ると全然だめだね
return 5 >>= (+2)
>>=
の先はまた同じ文脈にいれないといけないから
(ちなみに、Some 5
に2を足したいなら最初にやった(+2) <$> Some 5
だからね!すらすら書けるぜー)
じゃあ次に>>=
を試そう
Int -> Opt Int
の関数を適当に用意
let optInc x = Some $ x + 1
optInc 5
-- Some: 6
で、使う
Some 5 >>= optInc
-- Some: 6
return 5 >>= optInc
-- Some: 6
うん、出来ているね
return = Some
だから、pureの時の例と同じで、どっちも結果は同じ
ところで、Functorの時に出てきたoptHalfがInt -> Opt Int
だったなそういえば!
再掲
optHalf :: Int -> Opt Int
optHalf x = case even x of
True -> Some $ half x
_ -> None
return 6 >>= optHalf
-- Some: 3
お、ピッタリだ
んで、得た値がOptということは...
return 6 >>= optHalf >>= optHalf
-- None
あと
half
は奇数でも割っちゃうので、optHalf
を繰り返したい
出来たね!
Int -> Opt Int
なのに結果をどんどん次に連結している!(いちいちOptを引っぺがしている感じはないけど)
これもApplicativeFunctorと同じで、>>=
は文脈次第なので、EitherはまたOptとは違った感じになる
けど、ApplicativeFunctorで使った(+)
とは違い、最後に文脈に入れないといけないので関数は都度用意しないといけないのかな?
up :: Int -> Either String Int
up x
| x < 2 = Right $ x + 1
| otherwise = Left "too big"
down :: Int -> Either String Int
down x
| 0 < x = Right $ x - 1
| otherwise = Left "too small"
ある境界を越えると失敗するって感じ
使ってみよう
return 0 >>= inc
-- Right 1
return 0 >>= inc >>= inc
-- Right 2
return 0 >>= inc >>= inc >>= inc
-- Left "too big"
return 0 >>= inc >>= dec >>= dec
-- Left "too small"
return 0 >>= inc >>= dec >>= dec >>= inc >>= inc
-- Left "too small"
どのタイミングで失敗したかって気にしないでいくらでも連結できてる!
あと書いてて気付いたんだけど、returnが適切な文脈に突っ込んでくれるってことは、incとdecを何もEither専用にしないことが出来る気がする!
inc :: (Monad m) => Int -> m Int
inc x
| x < 2 = return $ x + 1
| otherwise = fail "too big"
dec :: (Monad m) => Int -> m Int
dec x
| 0 < x = return $ x - 1
| otherwise = fail "too small"
こうかー!
Some 1 >>= inc
-- Some: 2
Some 1 >>= inc >>= inc
-- None
Some 1 >>= inc >>= inc >>= dec
-- None
Someでも使えた!
returnはSomeで、failはNoneって実装だからね!
けどEitherのfailはerrorだった、Leftじゃあないのかー残念
Right 1 >>= inc >>= inc
-- *** Exception: too big
ところで、returnに慣れてきたので、以下は正しいけど
Some 5 >>= (\x -> return $ x + 1)
-- Some: 6
これはおかしいってのが、今ならわかる
Some 5 >>= (\x -> return None)
-- Some: None
returnはいつも使っているreturn文ではないから、戻す
ってことじゃあない!
Optの文脈においてはreturn = Some
だから、return None
ってしたらネストしちゃうね!
まとめ
class Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
-- (>>) と fail は略
- 文脈に入った値を、文脈を付ける関数に適用する
-
>>=
を使って適用する - returnは戻すって意味ではない
疑問
doが>>=
の糖衣構文ってのは聞いたことある
ここまで来たらdoも知りたい!
do記法
複数のモナド値を糊付けする
使う
>>=
での結合がネストしている場合があるとする
Some 3 >>= (\x -> Some "!" >>= (\y -> Some (show x ++ "!")))
-- Some: "3!"
これはもう当然って思えるけど、どこかがNoneになったら最終的にNoneね
None >>= (\x -> Some "!" >>= (\y -> Some (show x ++ "!")))
-- None
Some 3 >>= (\x -> None >>= (\y -> Some (show x ++ "!")))
-- None
これを
foo = do
x <- Some 3
y <- Some "!"
Some (show x ++ y)
こう書けるってのがdo記法らしい
まるで何も考えずにOptをはがしている様に見えるけど、do全体がOptの文脈内なので大丈夫、って感じかな
ghci> foo
Some: 7
>>=
と同じように、一部にNoneが入ったら最終的にはNoneにちゃんとなる
bar = do
x <- Some 3
y <- None
Some (show x ++ y)
ghci> bar
None
ちなみに、returnが戻すって意味ではない例
pon :: Opt Int
pon = do
x <- return 5
y <- return 6 >>= optHalf
return $ x + y
returnはこのOptの文脈においてはただのSomeだから、別に戻りはしないね!
pon :: Opt Int
pon = do
x <- return 5
return 5 >>= optHalf
y <- return 6 >>= optHalf
return $ x + y
こんな風に途中で失敗した場合、そこで戻っている様に見えるけど、ただ次の処理へNoneが伝播しているだけだと今ならわかる!
疑問: ApplicativeFunctorとの使い分け
でもさっきの例だと
(\x y -> show x ++ y) <$> Just 3 <*> Just "!"
でも同じ結果だよね
つまりApplicativeFunctorで十分って事?
Applicativeのススメ - あどけない話という今まで何度か見た記事にはApplicativeFunctorで良いよ、って書いてある(と思う)
慣れと状況次第、ってところもあるのかな?
(余談だけど、この記事が読める様になっていてとても嬉しいw)
疑問: IO
OptもIOもEitherも、なんかの文脈を付与しているって点では同列なんだな、って思ってきた
で、IOって文脈は、外の世界とやりとりできるって文脈ってことでおk?
baz :: IO Int
baz = do
print "hoge"
return 5
どこにもSomeって書いてないけど、Optの文脈だと型注釈を付ければSome 5
が得られる
poo :: Opt Int
poo = do
return 5
で、pooでprintを書いてみると
poo :: Maybe Int
poo = do
print "hoge"
return 5
怒られた、うん、知ってた
Maybeの文脈でIOは出来ないぞ、的なことを言われた気がする
逆のIOの文脈でMaybeでも怒られた
zak :: IO Int
zak = do
print "hoge"
x <- Just 5
return 5
でもこれなら怒られない
zak :: IO Int
zak = do
print "hoge"
print $ Just 5
return 5
ただ値の様に扱うのは良くて、<-
を使ってひっぺがすのは違う文脈の中では出来ない、って感じなのかな?
まとめ
まだまだ入り口って感じだけど、相当理解度があがった実感があるのでここまでで一区切りとするよ
学習って観点で大きく思ったことが2つ
- 絵とか例え話とかじゃあなくて、もうズバリ型をばーんと見せられるのが一番素直に理解できる気がした
- そしてその点で言うとH本は本当にわかりやすいと思った
- ただ目次的には
Functor
,ApplicativeFunctor
,Monad
は別に連続していないので、一気に理解したいぜ!ってのには向かないかも?
3つのまとめ(再掲)
3つの肝となる型をもう一度
Functor: 関数を、文脈のある値に適用できる
class Functor f where
fmap :: (a -> b) -> f a -> f b
fmap (+2) (Some 5)
-- Some: 7
ApplicativeFunctor: 文脈のある関数を、文脈のある値に適用できる
class (Functor f) => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
pure (+) <*> Some 2 <*> Some 3
-- Some 5
Monad: 文脈のある値を、文脈を付けて返す関数に適用する
class Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
-- (>>) と fail は略
let optInc x = Some $ x + 1
return 5 >>= optInc
-- Some: 6
### 今後の課題
とりあえず、気になっていること
- H本の
Monoid
の部分、飛ばしちゃった:p -
State
モナド等 - たまに見る
lift
って何? - モナドトランスフォーマーって何?
今までの「勘で>>=
とか<$>
とか使ってみて怒られる」という状態は脱したw
あとは経験あるのみ、かな?
ひとつふわっとした事が無くなってレベルアップした気がするので、とても満足
ではノシ