10
0

More than 3 years have passed since last update.

モナドプログラミング

Last updated at Posted at 2019-12-23

三島3大モナド

静岡のプログラマーの皆さん、こんにちは、アルチューハイマーの @karky7 です。

アルチューハイマーといえば、ピカチューよりかわいい生き物で有名なことで、皆さんご存知だと思われます。
そんな感じで、せっかくなのでHaskellの記事を書かさせていただきます。

Haskellの有名なモナドに

  • Stateモナド
    • 状態を引き回す際に利用します
  • Writerモナド
    • ログなどを追加していく際に利用します
  • Readerモナド
    • 一定の値を色々な処理へ配送する際に利用します

という三島3大ホルモンならぬ、3つのモナドがあります。(高麗は最近、お店を閉めたようで大変寂しく思います)

今回は、この中のReaderモナドをご紹介します。

まず、PHPのコード

HaskellっていっておいてPHPを書くのは、ポリ先生に叱られそうですが、最近ずっとPHPを書いていなかったため練習に書いてみました。

Xmas.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実行
$ php Xmas.php
Empty 吉田は@polidocの靴下にビールを詰め込んだ。
Empty 吉田は@secondarykeyの靴下に米焼酎の安いのをぶち込んだ。
Empty 吉田は日本酒でやっつけようと思い@ftnkの靴下にねじ込んだ。

お酒を配るためにサンタのEmpty 吉田さんは、それぞれのお家へお酒を配送しています、これをHaskell的にやるとどうなるか、Readerモナドを使ってやってみます。

非Readerモナド版

Readerモナドを見る前に、Readerモナドを使わない普通のHaskellコードだとこんな感じになります。

Xmax1.hs
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型
Reader e a = Reader { runReader :: e -> a }

Readerの型は、e(環境)と何かの型から成り、Readerの中に e(環境)を引数にとってa(何かの型)を返す関数として定義されています。そしてrunReader関数はReaderモナドからその関数を取り出す取得関数です、色々なモナドはrunXXXXXのようなモナドを走らせる意味の関数を持っています、runStateやrunWriterなど。

reader関数
reader :: (e -> a) -> Reader e a

続いて、reader関数は(e -> a)の関数を引数にとって、Readerモナドを返します、Readerモナドを構築する関数です。この2つを使ってPHPのサンプルをReaderモナドを使ったコードへ書き換えます。

Readerモナド版

では、Readerモナドで書き換えてみます。

Xmas2.hs
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 aeSantaa[String] だと見てもらえればわかりやすい?と思います。それをrunReader関数で中の(e -> a)を取り出し、その関数へSanta "Empty 吉田" サンタをお家へ侵入させています。

あと気になるのが、SantaがsantaWork関数で見えてないことですね、サンタのお仕事から、サンタが消えてますね...、これについては後ほど説明します。

gentoo!
pol <- polidogHouse

のような書き方はモナドを利用したdo構文で、Readerモナドの(e -> a)へSantaを適用したa(計算結果)を取り出しています。

return
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クラスはというと、以下のように定義されています、要はモナドにするにはこの関数を実装してねって事です。

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
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クラスのインスタンスとして以下のように定義されています。

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
 (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らしく、以下のように定義されています

ReaderT
newtype ReaderT r (m :: * -> *) a = ReaderT { runReaderT :: r -> m a }

mのところが置き換え可能ということです、そして、実は、Readerモナドは、

Reader
type Reader r = ReaderT r Identity :: * -> *

mの部分をIdentityで具体化したもので、Identityは恒等モナドといい、計算戦略を内包しないモナドです。
純粋なReaderモナドの定義とでもいうのでしょうか...

本題に戻って、Readerと一緒にIOを合成する場合、mを同じようにIOで置き換えます。

ReaderT
newtype ReaderT r IO a = ReaderT { runReaderT :: r -> IO a }

(m :: * -> *)という表記は、mに入れるモナドは、型引数が(* -> *)であることが必要ということを意味し

kind
ghci > :k IO
IO :: * -> *

という種(kind)になっているため利用可能というわけです、IO意外のモナドも同じように扱うことが可能です、ただしIOだけはちょっと特殊で、モナドを合成する場合、一番下に配置する必要があります、これはルールらしいです(忘れました)。

ということでこれを使って

Xmas3.hs
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関数です、こんな感じで取得できます。

ask
(Santa name) <- ask

続いて、IO処理をReaderモナドの中で実行する方法ですが、型やコードの文脈に厳格なHaskellでは、そのままIOな関数をReaderモナドの中で発行することはできません、そこでliftIOというリフト関数を利用します。

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を確認すると

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のインスタンスなれることを意味しています。

info
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 吉田

jiji-shizuoka_back_white.png

10
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
0