三島3大モナド
静岡のプログラマーの皆さん、こんにちは、アルチューハイマーの @karky7 です。
アルチューハイマーといえば、ピカチューよりかわいい生き物で有名なことで、皆さんご存知だと思われます。
そんな感じで、せっかくなのでHaskellの記事を書かさせていただきます。
Haskellの有名なモナドに
- Stateモナド
- 状態を引き回す際に利用します
- Writerモナド
- ログなどを追加していく際に利用します
- Readerモナド
- 一定の値を色々な処理へ配送する際に利用します
という三島3大ホルモンならぬ、3つのモナドがあります。(高麗は最近、お店を閉めたようで大変寂しく思います)
今回は、この中のReaderモナドをご紹介します。
まず、PHPのコード
HaskellっていっておいてPHPを書くのは、ポリ先生に叱られそうですが、最近ずっとPHPを書いていなかったため練習に書いてみました。
<?php
class Santa {
protected $name;
public function __construct($name) {
$this->name = $name;
}
public function getName() {
return $this->name;
}
}
function polidogHouse($santa) {
$name = $santa->getName();
return ($name . "は@polidocの靴下にビールを詰め込んだ。\n");
}
function secondarykeyHouse($santa) {
$name = $santa->getName();
return ($name . "は@secondarykeyの靴下に米焼酎の安いのをぶち込んだ。\n");
}
function ftnkHouse($santa) {
$name = $santa->getName();
return ($name . "は日本酒でやっつけようと思い@ftnkの靴下にねじ込んだ。\n");
}
$santa = new Santa('Empty 吉田');
$polidog = polidogHouse($santa);
$secondarykey = secondarykeyHouse($santa);
$ftnk = ftnkHouse($santa);
print($polidog . $secondarykey . $ftnk);
実行すると
$ php Xmas.php
Empty 吉田は@polidocの靴下にビールを詰め込んだ。
Empty 吉田は@secondarykeyの靴下に米焼酎の安いのをぶち込んだ。
Empty 吉田は日本酒でやっつけようと思い@ftnkの靴下にねじ込んだ。
お酒を配るためにサンタのEmpty 吉田さんは、それぞれのお家へお酒を配送しています、これをHaskell的にやるとどうなるか、Readerモナドを使ってやってみます。
非Readerモナド版
Readerモナドを見る前に、Readerモナドを使わない普通のHaskellコードだとこんな感じになります。
data Santa = Santa String deriving(Show)
main :: IO()
main = do
let santa = Santa "Empty 吉田"
pol = polidogHouse santa
sec = secondarykeyHouse santa
ftn = ftnkHouse santa
putStr $ pol ++ sec ++ ftn
polidogHouse :: Santa -> String
polidogHouse (Santa name) = name ++ "は@polidocの靴下にビールを詰め込んだ。\n"
secondarykeyHouse :: Santa -> String
secondarykeyHouse (Santa name) = name ++ "は@secondarykeyの靴下に米焼酎の安いのをぶち込んだ。\n"
ftnkHouse :: Santa -> String
ftnkHouse (Santa name) = name ++ "は日本酒でやっつけようと思い@ftnkの靴下にねじ込んだ。\n"
実行すると
$ runghc Xmax1.hs
Empty 吉田は@polidocの靴下にビールを詰め込んだ。
Empty 吉田は@secondarykeyの靴下に米焼酎の安いのをぶち込んだ。
Empty 吉田は日本酒でやっつけようと思い@ftnkの靴下にねじ込んだ。
PHPと同じです、ただサンタさんを全ての関数に引数として渡しています、サンタがEmpty 吉田ってバレちゃいますね。
Readerモナドについてざっくり
Readerモナドは以下のように定義されています
Reader e a = Reader { runReader :: e -> a }
Readerの型は、e(環境)と何かの型から成り、Readerの中に e(環境)を引数にとってa(何かの型)を返す関数として定義されています。そしてrunReader関数はReaderモナドからその関数を取り出す取得関数です、色々なモナドはrunXXXXXのようなモナドを走らせる意味の関数を持っています、runStateやrunWriterなど。
reader :: (e -> a) -> Reader e a
続いて、reader関数は(e -> a)の関数を引数にとって、Readerモナドを返します、Readerモナドを構築する関数です。この2つを使ってPHPのサンプルをReaderモナドを使ったコードへ書き換えます。
Readerモナド版
では、Readerモナドで書き換えてみます。
import Control.Monad.Reader
data Santa = Santa String deriving(Show)
main :: IO()
main = mapM_ putStr $ runReader santaWork $ Santa "Empty 吉田"
santaWork :: Reader Santa [String]
santaWork = do
pol <- polidogHouse
sec <- secondarykeyHouse
ftn <- ftnkHouse
return [pol, sec, ftn]
polidogHouse :: Reader Santa String
polidogHouse = reader $ \(Santa name) -> name ++ "は@polidocの靴下にビールを詰め込んだ。\n"
secondarykeyHouse :: Reader Santa String
secondarykeyHouse = reader $ \(Santa name) -> name ++ "は@secondarykeyの靴下に米焼酎の安いのをぶち込んだ。\n"
ftnkHouse :: Reader Santa String
ftnkHouse = reader $ \(Santa name) -> name ++ "は日本酒でやっつけようと思い@ftnkの靴下にねじ込んだ。\n"
実行すると
$ runghc Xmas2.hs
Empty 吉田は@polidocの靴下にビールを詰め込んだ。
Empty 吉田は@secondarykeyの靴下に米焼酎の安いのをぶち込んだ。
Empty 吉田は日本酒でやっつけようと思い@ftnkの靴下にねじ込んだ
santaWork関数がサンタのお仕事の内容です、santaWork関数の型は、Reader Santa [String]で、Santaが環境で、[String]はサンタのお仕事完了リストとでもいいますかね、それを返しています。
Reader Santa [String] の見方としては、Reader e a の e が Santa で a が [String] だと見てもらえればわかりやすい?と思います。それをrunReader関数で中の(e -> a)を取り出し、その関数へSanta "Empty 吉田" サンタをお家へ侵入させています。
あと気になるのが、SantaがsantaWork関数で見えてないことですね、サンタのお仕事から、サンタが消えてますね...、これについては後ほど説明します。
pol <- polidogHouse
のような書き方はモナドを利用したdo構文で、Readerモナドの(e -> a)へSantaを適用したa(計算結果)を取り出しています。
return [pol, sec, ftn]
そして、最後のreturnですが、これは値を返しているのではなく、計算した結果をReaderモナドへ戻しているコードになります。santaWork関数の型が、Reader Santa [String]であるのはそういうことです。そしてsantaWork関数のdo構文は実際にはラムダ式の見やすい版で、こういう書き方でも同じです。
santaWork :: Reader Santa [String]
santaWork =
polidogHouse >>= \pol ->
secondarykeyHouse >>= \sec ->
ftnkHouse >>= \ftn -> return [pol, sec, ftn]
doが消えて、ラムダ式でつながった式になります。
もうちょっとモナド
モナドといわれるものは全てMonadクラスのインスタンスになっています。
そしてMonadクラスはというと、以下のように定義されています、要はモナドにするにはこの関数を実装してねって事です。
class Applicative m => Monad (m :: * -> *) where
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
return :: a -> m a
順に説明しますと
return :: a -> m a
returnはaという型の値をモナドの中に入れる関数で、注入関数とかいわれてます。
(>>=) :: m a -> (a -> m b) -> m b
これはモナドに包まれている(m a)からaを取り出して(a -> m b)という関数の入力へ適用し、(m b)というモナドに包まれた値を関数の定義です。
(>>) :: m a -> m b -> m b
これは、(m a)を評価し、続いて(m b)を評価する単純な関数です、(>>=)のように値を次の関数へ渡すようなことはしません。
そしてReaderモナドもMonadクラスのインスタンスとして以下のように定義されています。
instance Monad (Reader e) where
return a = Reader $ \e -> a
(Reader r) >>= f = Reader $ \e -> runReader (f (r e)) e
returnは先ほどいったように、Readerへ戻しているもの、(>>=)はMonadの定義から、
(Reader r) >>= f
で、(m a)がReader rで、fが(a -> m b)という見方をしてもらえれば理解しやすいかもしれません。
(Reader r) >>= f = Reader $ \e -> runReader (f (r e)) e
- r は (e -> a)
- fは (a -> Reader r') を返す関数
と見れます、そこで、文章で説明すると、まず、内側の、(r e)を適用して計算結果を求めて、それを 関数 f に適用すると Reader r' が返ってくる、その Reader r' にrunReaderを適用し、中の(e -> a)をさらに取り出し、その関数へ e をさらに適用すると計算結果 a' が求まる、最終的に
(Reader r) >>= f = Reader $ \e -> a'
この環境(e)を引き回す導線をReaderモナドが後ろで行っているために、サンタが見えなくなっています。
つづいてReaderTモナド
汎用的なReaderモナドとして、ReaderTがあります。
例えば、サンタが、プレゼントを配っている最中に、腹がへってコンビニとか行きたくなった場合どうする?
みたいな事がプログラムしてると起こります。
サンタは酒を配ることが絶対的な仕事
でも腹が減ったので、酒を配るという仕事の中で(Readerモナドの中で)、コンビニにいきたい(IOを発行したい)
こういう場合は、Monad transformers っていうものを使って、モナドを合成することで対応できます(合成といって良いかは不明)。
ReaderTのTはTransformersのTらしく、以下のように定義されています
newtype ReaderT r (m :: * -> *) a = ReaderT { runReaderT :: r -> m a }
mのところが置き換え可能ということです、そして、実は、Readerモナドは、
type Reader r = ReaderT r Identity :: * -> *
mの部分をIdentityで具体化したもので、Identityは恒等モナドといい、計算戦略を内包しないモナドです。
純粋なReaderモナドの定義とでもいうのでしょうか...
本題に戻って、Readerと一緒にIOを合成する場合、mを同じようにIOで置き換えます。
newtype ReaderT r IO a = ReaderT { runReaderT :: r -> IO a }
(m :: * -> *)という表記は、mに入れるモナドは、型引数が(* -> *)であることが必要ということを意味し
ghci > :k IO
IO :: * -> *
という種(kind)になっているため利用可能というわけです、IO意外のモナドも同じように扱うことが可能です、ただしIOだけはちょっと特殊で、モナドを合成する場合、一番下に配置する必要があります、これはルールらしいです(忘れました)。
ということでこれを使って
import Control.Monad.Reader
data Santa = Santa String deriving(Show)
main :: IO ()
main = do
(msg, meryXmas) <- runReaderT santaWork $ Santa "Empty 吉田"
putStr msg
putStr meryXmas
santaWork :: ReaderT Santa IO (String, String)
santaWork = polidogHouse >>= secondarykeyHouse >>= ftnkHouse
polidogHouse :: ReaderT Santa IO (String, String)
polidogHouse = do
(Santa name) <- ask
return (name ++ "は@polidocの靴下にビールを詰め込んだ。\n", "メリー")
secondarykeyHouse :: (String, String) -> ReaderT Santa IO (String, String)
secondarykeyHouse (msg, msg') = do
(Santa name) <- ask
liftIO $ putStr "ローソンで一服、ビール飲みたいけど、トナカイの飲酒運転になるからやめておこう\n"
return (msg ++ (name ++ "は@secondarykeyの靴下に米焼酎の安いのをぶち込んだ。\n"), msg' ++ "クリスマス")
ftnkHouse :: (String, String) -> ReaderT Santa IO (String, String)
ftnkHouse (msg, msg') = do
(Santa name) <- ask
return (msg ++ name ++ "は日本酒でやっつけようと思い@ftnkの靴下にねじ込んだ。\n", msg' ++ " Bye 2019...\n")
こんな感じになります、ReaderTの文脈の中でIO処理を実現しています、ちょうど @secondarykey に酒を届けるときにローソンでビールを飲もうとしてますね。
それから、Readerモナドのなかで環境変数へアクセスする方法もあります、それが ask関数です、こんな感じで取得できます。
(Santa name) <- ask
続いて、IO処理をReaderモナドの中で実行する方法ですが、型やコードの文脈に厳格なHaskellでは、そのままIOな関数をReaderモナドの中で発行することはできません、そこでliftIOというリフト関数を利用します。
Prelude Control.Monad.Reader> :t liftIO
liftIO :: MonadIO m => IO a -> m a
これはmがMonadIOのインスタンスであれば、IO aな関数(putStr)を(m a)の文脈(ここでmはReader rですね)へリフトアップする関数です。
ちなみに、ReaderTのinfoを確認すると
ghci> :i ReaderT
newtype ReaderT r (m :: * -> *) a
= ReaderT {runReaderT :: r -> m a}
-- Defined in ‘Control.Monad.Trans.Reader’
instance [safe] MonadIO m => MonadIO (ReaderT r m)
-- Defined in ‘Control.Monad.Trans.Reader’
...
...
mがMonadIOのインスタンスであれば、ReaderT r mもMonadIOのインスタンスなれることを意味しています。
ghci> :i MonadIO
class Monad m => MonadIO (m :: * -> *) where
liftIO :: IO a -> m a
{-# MINIMAL liftIO #-}
-- Defined in ‘Control.Monad.IO.Class’
instance [safe] MonadIO m => MonadIO (ReaderT r m)
-- Defined in ‘Control.Monad.Trans.Reader’
instance [safe] MonadIO IO -- Defined in ‘Control.Monad.IO.Class’
IOモナドもMonadIOのインスタンスになっていますね。
$ runghc Xmas3.hs
ローソンで一服、ビール飲みたいけど、トナカイの飲酒運転になるからやめておこう
Empty 吉田は@polidocの靴下にビールを詰め込んだ。
Empty 吉田は@secondarykeyの靴下に米焼酎の安いのをぶち込んだ。
Empty 吉田は日本酒でやっつけようと思い@ftnkの靴下にねじ込んだ。
メリークリスマス Bye 2019...
Haskellで書くとこんな感じになるよっていうサンプルでした。
やっぱりHaskellの良いところは、厳格な型システムの上に成り立っているところが一番ではないでしょうか。
ちょっとアプローチが違う関数型言語ですが、皆さんにも、これを期に、Haskellやってみてはいかがでしょうか?
説明が雑で申し訳ないのですが、また今後のHaskell勉強会のShizioka.hsで詳しくやっていきたいとおもいますのでよろしくお願いします。
それからShizuoka.hsについて
三島Haskell無名関数の会から、紆余曲折をへて、Shizuoka.hs を立ち上げてはみました。
色々ありましたね、「勉強会やるぞぉ」っていってconnpass立てたら、集まったのは自分合わせて二人だけで、勉強会という名の飲み会にチェンジし半日のんだくれたとか、あの当時は日本酒党って謎の政党に所属しないとHaskell書けませんでしたし。。。まぁよかったです、とりあえず第1回が開催できて。
それから、Shizuoka.hsに参加していただいたた方、また発表者のみなさんにつきましてもありがとうございました、当日はハイマーのため歩行困難となり、お別れの際にお礼をいい忘れていましたのでこの場でお礼申し上げます。
勉強会も楽しいのですが、言語関係なくエンジニア同士での飲みながらコンピューティングの話をつまみに盛り上がるのが楽しくてやっているのが90%以上で、そういう環境はできる限り継続していきたいなぁと思っています。
言語も色々やっておいた方が損はないし、学生の皆さんもShizuoka.hsでワイワイやってもらえると静岡の関数型言語界隈もオモローってなっていくんじゃないかと思っています。
また、Shizuoka.hs でアルチューハイマーな夜を楽しみましょう、では、彼女がいる人も、バツイチも、孤独な人も...
メリークリスマス by Empty 吉田