カテゴリ:Haskell( 762 )

IOモナドでスタックを利用する

StateT モナド変換子を使って、IOモナドにスタックを導入してみた。また、ついでにスタックを使ったポーランド記法電卓を作った。ポーランド記法については、ネットで検索できる。実行例は次のようになる。極力いらない要素を絞ったつもりだが、わかりづらいかもしれない。とにかく、IOモナドに簡単にスタックを導入できることはわかった。

$ runghc stack.hs
calculator> * + 1 2 + 3 4
21
calculator> quit

プログラムのソースは次のようになる。

import Control.Monad.State

type Stack a = StateT [String] IO a

push :: String -> Stack ()
push x = do
  ss <- get
  put (x:ss)

pop :: Stack String
pop = do
  ss <- get
  put (tail ss)
  return (head ss)

evaluate :: Stack String
evaluate = do
  x <- pop
  case x of
    "+" -> do
      y <- evaluate
      z <- evaluate
      return $ show $ (read y :: Int) + (read z :: Int)

    "*" -> do
      y <- evaluate
      z <- evaluate
      return $ show $ (read y :: Int) * (read z :: Int)

    _ -> return x

main = do
  putStr "calculator> "
  cs <- getLine
  if cs == "quit"
    then return ()
    else do
      putStrLn =<< evalStateT evaluate (words cs)
      main

by tnomura9 | 2018-12-23 19:15 | Haskell | Comments(0)

return 関数とモナドスタック

モナド変換子の使い方をいろいろと試していたら、複合モナドのモナド値を作るのは return 関数と type signature を組み合わせるのが一番簡単だということがわかった。それと同時に、runStateT 関数のような複合モナドのコンテナを取り出す関数が return とは逆の操作に使えることもわかった。この2つを組み合わせると、あれほど不可解に見えたモナド変換子の使い方がスッキリとわかる。つまり、

1)値 a をモナドで包む ---> foo = (return 2) :: StateT s Maybe Int
2)モナド値 (StateT s Mabye) 2 から複合モナド (StateT s Maybe) のコンテナの値を取り出す ---> runStateT foo (これは s -> m (a,s) 型の関数を取り出すだけなので、実際に端末にデータを表示するには状態 s = 3 を与えて runState foo 3 としなくてはならない。)

という2つのポイントがわかれば、モナド変換子を簡単に扱うためのイメージを作ることができる。言葉で説明すると煩雑になるので実際に StateT s Mabe Int 型のモナド値 foo を作り、逆に foo のコンテナのデータを取り出して利用してみる。

Prelude> import Control.Monad.State

Prelude Control.Monad.State> foo = (return 2) :: StateT s Maybe Int
Prelude Control.Monad.State> :t foo
foo :: StateT s Maybe Int

Prelude Control.Monad.State> :t runStateT foo
runStateT foo :: s -> Maybe (Int, s)
Prelude Control.Monad.State> runStateT foo 3
Just (2,3)

これに加えて、StateT s Maybe Int 型のような複雑な表記の複合モナドのなかで、内側の Maybe モナドの値を扱いたいときは return 関数の代わりに lift 関数を使うことがわかると、State モナドと Maybe モナドを組み合わせて扱うのは、do 記法によって簡単にできる。

Prelude Control.Monad.State> runStateT (do x <- lift $ Just 2; y <- get; return (x+y)) 3
Just (5,3)

たとえば、IOモナドの中に State モナドを導入するようなことはこれだけで簡単にできる。

Prelude Control.Monad.State> runStateT (do x <- lift $ getLine; y <- get; lift $ putStrLn (x++y)) "world"
hello
hello world
((),"world")

ところで、Just 2 のような単純な Maybe モナドもモナドなら、StateT s Maybe Int のような複合モナドもモナドである。したがって、StateT s Maybe Int モナドも他のたとえば ExceptT モナド変換子と組み合わせて複合モナド ExceptT e (StateT s Maybe) Int モナドを作ることができるはずだ。このような複合モナドでは Except モナドの関数と、State モナドの関数と、Maybe モナド値を組み合わせて使うことができる。そうして、それは実際に可能だ。複合モナドの複合モナドのような複雑なものも、return 関数と type signature を使うと作ることができるからだ。

Prelude Control.Monad.State> import Control.Monad.Except
Prelude Control.Monad.State Control.Monad.Except> bar = (return 2) :: ExceptT e (StateT s Maybe) Int
Prelude Control.Monad.State Control.Monad.Except> :t bar
bar :: ExceptT e (StateT s Maybe) Int

問題は、このようなモナドのコンテナの値をどのようにして取り出すかだが、それは runStateT 関数と runExceptT 関数を入れ子にして使えばいい。

Prelude Control.Monad.State Control.Monad.Except> :t runExceptT bar
runExceptT bar :: StateT s Maybe (Either e Int)

Prelude Control.Monad.State Control.Monad.Except> :t runStateT $ runExceptT bar
runStateT $ runExceptT bar :: s -> Maybe (Either e Int, s)

Prelude Control.Monad.State Control.Monad.Except> runStateT (runExceptT bar) 3
Just (Right 2,3)

キー入力が面倒なので runStateT 関数と runExcept 関数の合成関数を作ってしまう。

Prelude Control.Monad.State Control.Monad.Except> hoge = runStateT . runExceptT
Prelude Control.Monad.State Control.Monad.Except> hoge bar 3
Just (Right 2,3)

この hoge 関数を使って ExceptT e (StateT s Maybe) Int モナドの lift 関数の動作を見てみよう。

Prelude Control.Monad.State Control.Monad.Except> hoge (do x <- lift $ get; return x) 3
(Right 3,3)

ExceptT e (StateT s Maybe) Int モナドのすぐ内側のモナドは StateT s Maybe モナドなので StateT モナドの関数 get を使うためにはこれを lift してやる必要がある。それでは ExceptT e (StateT s Maybe) Int モナドの内側の StateT s Maybe モナドの、その内側の Maybe モナドのコンテナの値を最外層の複合モナドで利用するためにはどうすればいいのだろうか。それは、lift を2回使えばいいのだ。

Prelude Control.Monad.State Control.Monad.Except> hoge (do x <- lift $ lift $ Just 2; return x) 3
Just (Right 2,3)

このように、lift 関数を活用することによって複合モナドのどの階層のモナドのデータも最外層のデータとして活用することができる。例えば次のようなプログラムも実行することができる。

Prelude Control.Monad.State Control.Monad.Except> hoge (do x <- lift $ lift $ Just 2; y <- lift $ get; return (x+y)) 3
Just (Right 5,3)

モナド演算子を使って複合モナドを作り、複数のモナドの機能を統合して使う方法といっても、これだけのことだ。それは、次の3点だけがわかっていればよい。1つめは return 関数と type signature の組み合わせでモナド値を作ることができること、2つめは、StateT 関数などのアクセサはモナドのコンテナの値を取り出していること、3つめは lift 関数を利用することで、インナーモナドの関数を利用できるということの3つだ。

モナド演算子という概念の見かけ上の複雑さに惑わされず、上の3つのポイントを押えることで、複合モナドスタックを自在に操ることができるようになる。

by tnomura9 | 2018-12-16 05:21 | Haskell | Comments(0)

type signature で ExceptT モナド値を作る

前回は StateT モナド変換子による複合モナドのモナド値を type signature で作ってみたが、今回は ExceptT モナド変換子による複合モナドのモナド値を作ってみる。

まずは、type signature ではなく、データコンストラクタによる Except 複合モナドのモナド値を作ってみる。

Prelude> import Control.Monad.Except

Prelude Control.Monad.Except> :t ExceptT
ExceptT :: m (Either e a) -> ExceptT e m a

Prelude Control.Monad.Except> foo = ExceptT (Just (Right (2::Int)))
Prelude Control.Monad.Except> runExceptT foo
Just (Right 2)

次に、return 関数を使って ExceptT の複合モナドの値を作る。これには type signature が必要になる。

Prelude Control.Monad.Except> bar = (return 2) :: ExceptT String Maybe Int
Prelude Control.Monad.Except> runExceptT bar
Just (Right 2)

さらに、lift 関数を使って EcxeptT の複合モナドの値を作る。

Prelude Control.Monad.Except> baz = (lift $ Just 2) :: ExceptT String Maybe Int
Prelude Control.Monad.Except> runExceptT baz
Just (Right 2)

最後に、throwError 関数を使って ExceptT の複合モナドを作る。

Prelude Control.Monad.Except> qux = (throwError "hello") :: ExceptT String Maybe Int
Prelude Control.Monad.Except> runExceptT qux
Just (Left "hello")

これを見ると ExceptT モナド変換子の複合モナドのモナド値を作るには、直接的なデータを使うときは ExceptT データコンストラクタで作り、return, lift, throwError などのモナド関数を利用するときは Except e m a タイプシグネチャーを利用すればいいことがわかる。

以上のことがわかると、次のような do 記法で作成された、複数の式による複合モナドのプログラムの動作の意味がわかってくる。

Prelude Control.Monad.Except> runExceptT (do x <- lift $ Just 2; return x)
Just (Right 2)
Prelude Control.Monad.Except> runExceptT (do x <- lift Nothing; return x)
Nothing


by tnomura9 | 2018-12-15 07:34 | Haskell | Comments(0)

return 関数と複合モナド

モナドの return 関数は抽象的な関数で、「データをモナドでラッピングする」という意味がある。(return 2) という値は type signatuer で指定しなければどのモナド値をとるのかわからない。たとえば Maybe モナドを指定すると次のように Maybe 型の値を返す。

Prelude> (return 2) :: Maybe Int
Just 2

それでは複合モナドについても、同様に return 2 の戻り値を type signature で指定できるのだろうか。それは、簡単にできる。

return 2 の戻り値を type signature で StateT s Maybe Int 型のデータに指定するには次のようにする。

Prelude> import Control.Monad.State
Prelude Control.Monad.State> foo = (return 2) :: StateT s Maybe Int
Prelude Control.Monad.State> :t foo
foo :: StateT s Maybe Int
Prelude Control.Monad.State> runStateT foo 3
Just (2,3)

StateT s [] Int 型にするには次のようにする。

Prelude Control.Monad.State> bar = (return 2) :: StateT s [] Int
Prelude Control.Monad.State> :t bar
bar :: StateT s [] Int
Prelude Control.Monad.State> runStateT bar 3
[(2,3)]

StateT s (Either String) Int 型にするには次のようにする。

Prelude Control.Monad.State> buz = (return 2) :: StateT s (Either String) Int
Prelude Control.Monad.State> :t buz
buz :: StateT s (Either String) Int
Prelude Control.Monad.State> runStateT buz 3
Right (2,3)

lift 関数についても同様のことができる。

Prelude Control.Monad.State> qux = (lift $ Left "hello") :: StateT s (Either String) Int
Prelude Control.Monad.State> :t qux
qux :: StateT s (Either String) Int
Prelude Control.Monad.State> runStateT qux 3
Left "hello"

このように、(return 2) は StateT s Maybe Int のような複雑な複合モナドの型にも type signature で指定できることがわかる。


by tnomura9 | 2018-12-14 22:43 | Haskell | Comments(0)

StateT変換子の RunStateT 関数

ghci をいじってみて StateT モナド変換子が実は、データコンストラクタだということが分かった。

Prelude> import Control.Monad.State
Prelude Control.Monad.State> foo = StateT (\s -> Just (2,s))
Prelude Control.Monad.State> :t foo
foo :: Num a => StateT b Maybe a

つまり、StateT はコンテナに s -> m (a, s) 型の関数を一つだけ入れることのできるデータコンストラクタなのだ。つまり、StateT b Maybe a という複合モナドは、実態は、StateTデータコンストラクタに (\s -> Just (2,s)) という関数を入れた代数的データ型のデータなのだ。

それでは runStateT とはどんな関数なのだろうか。上の文脈でいうと、それは StateT データコンストラクタのコンテナの中にある関数を取り出すアクセサ関数だ。別の言い方をすると、StateT データコンストラクタのフィールドラベルが runStateT であるということだ。つまり、

StateT {runStateT :: (\s -> Just (2,s))}

こういう風に考えると、StateT b Maybe a という複合モナドのデータ型も、普通のデータ型と同じに見えてくる。実際にそれは次のように確かめることができる。

Prelude Control.Monad.State> :t runStateT foo
runStateT foo :: Num a => s -> Maybe (a, s)

runStateT foo の方は s -> Maybe (a,s) 型の関数だ。したがってその引数 s に値を与えてやると Maybe (a, s) 型のデータが得られるはずだ。

Prelude Control.Monad.State> runStateT foo 3
Just (2,3)

この辺りは、以前のバージョンの State モナドの仕組みと全く同じだ。それだけでなく、StateT データコンストラクタの場合は、任意のモナドを含めることができる、State モナドの拡張版になっている。

Prelude Control.Monad.State> bar = StateT (\s -> Right (2, s))
Prelude Control.Monad.State> runStateT bar 3
Right (2,3)

したがって、新しい State モナドの定義はStateTモナドを使って作るように変更されている。StateTデータコンストラクタに Identity モナドを含めるのだ。

Prelude Control.Monad.State> import Control.Monad.Identity
Prelude Control.Monad.State Control.Monad.Identity> baz = StateT (\s -> Identity (2,s))
Prelude Control.Monad.State Control.Monad.Identity> runStateT baz 3
Identity (2,3)

ペア (2,3) が Identity データコンストラクタのコンテナに入っているが、runIdentity アクセサを利用すれば、オールドバージョンの State モナドのように生のペアを取り出すことができる。

Prelude Control.Monad.State Control.Monad.Identity> runIdentity $ runStateT baz 3
(2,3)

したがって、新しい定義では State データコンストラクタはなく、state 関数がその役割を果たしている。

Prelude Control.Monad.State Control.Monad.Identity> moo = state (\s -> (2,s))
Prelude Control.Monad.State Control.Monad.Identity> :t moo
moo :: (MonadState s m, Num a) => m a
Prelude Control.Monad.State Control.Monad.Identity> runState moo 3
(2,3)

このようにモナド変換子 StateT は State モナドの改良版であったはずだが、今では State モナドは StateT モナドの特殊な場合になっている。また、古いバージョンの State モナドが理解できていれば、同じような発想で StateT モナドが理解できることが分かる。

by tnomura9 | 2018-12-12 23:47 | Haskell | Comments(0)

モナド変換子はデータ型だ

モナド変換しの実態がもう一つ分からないので StateT についていろいろテストしてみた。

まずは、StateT の型を調べてみた。

Prelude> import Control.Monad.State
Prelude Control.Monad.State> :t StateT
StateT :: (s -> m (a, s)) -> StateT s m a

これは StateT が (s -> m (a,s)) 型の関数を引数にとるデータ構築子であることを示している。そこで、実際に (\s -> Just (2,s)) という関数を StateT に渡してモナド値を作ってみた。

Prelude Control.Monad.State> foo = StateT (\s -> Just (2,s))
Prelude Control.Monad.State> :t foo
foo :: Num a => StateT b Maybe a

すると、foo は StateT b Maybe a 型のモナド値になった。概念的には (StateT b Maybe) というモナド型のデータコンストラクタのコンテナに値 a が入っているとイメージできる。モナド値がイメージできたのでこれを do 記法の中で使ってみた。

Prelude Control.Monad.State> runStateT foo 3
Just (2,3)
Prelude Control.Monad.State> runStateT (do x <- foo; y <- get; return (x*y)) 3
Just (6,3)
Prelude Control.Monad.State> runStateT (do x <- lift $ Nothing; y <- get; return (x*y)) 3
Nothing

確かに (StateT b Maybe) 型のデータコンストラクタは State モナドも Maybe モナドも同時に使うことができるのが分かる。StateT b Maybe a 型のデータがモナド値であることが分かったので、StateT モナド変換子のなかに StateT b Mayb a のデータを入れ子にして内包させてみた。

Prelude Control.Monad.State> :t runStateT (lift foo) 5
runStateT (lift foo) 5 :: (Num a, Num s) => StateT b Maybe (a, s)
Prelude Control.Monad.State> runStateT (runStateT (lift foo) 5) 3
Just ((2,5),3)

最後の例はわかりにくいかもしれないが、最初に StateT と Maybe モナドの複合モナド StateT b Maybe a を作って、さらに StateT と StateT b Maybe b モナドの複合モナドを作るというイメージが操作的にできれば、複数のモナドを利用する複合モナドが使えるようになる。最後の runStateT を入れ子に使っているところが StateT c (StateT b Maybe) a モナドのコンテナの値を取り出す方法だ。


by tnomura9 | 2018-12-12 05:55 | Haskell | Comments(0)

モナドの return 関数

モナドの return 関数は一種の抽象関数だ。どんなモナドのプログラムにも出現して、値をモナドに包んで返すという抽象的な働きをしている。それは、リターン関数をいろいろなモナドに使用した次の実行例からもわかる。

Prelude> return 5 :: Maybe Int
Just 5
Prelude> return 5 :: [Int]
[5]
Prelude> return 5 :: Either () Int
Right 5

なぜこのようなことができるかと言うと、モナドの関数は全て M a 型の値を返すからだ。モナドとは概念的には、唯一つのコンテナを持つデータコンストラクタ M a のモナド値と a -> M b 型の引数を一つ取りモナド値を返すモナド関数を、バインド演算子で結合したものと思うとわかりやすい。Ma >>= foo は M b 型の値を返す。M b 型もやはりモナド値なので、>>= は次のように次々に連結していくことができる。

Ma >>= foo >>= bar >>= ... >>= buz

どのように連結していってもモナドのプログラムが返すのは M a というモナド値であることには変わりない。したがって、return 関数はどのようなモナドについても生のデータをモナドでラッピングすることができる。

ようするにモナドとは、モナド値 M a、モナド関数 a -> M b、return 関数、バインド演算子 >>= からのみ構成されているプログラムだ。これが端的に現れるのは Identity モナドのプログラムだ。Maybe モナドなどには失敗値である Nothing の処理ができるなど固有の機能があるが、Identity モナドにはそのようなものは全く無い単に最も基本的なモナドとして働くだけだ。したがって、Identityモナドで動くブログラムは原則どのようなモナドでも動く。

たとえば、次のプログラム foo は do 記法と <- とreturn からしかできていない。これは Identity モナドのプログラムとして働く。

Prelude> import Control.Monad.Identity
Prelude Control.Monad.Identity> foo x = do y <- return 10; return (x*y)
Prelude Control.Monad.Identity> foo 5 :: Identity Int
Identity 50

しかし、これは同時にこのプログラムが Maybe モナドや Either モナドでも動くことを意味している。

Prelude Control.Monad.Identity> foo 5 :: Maybe Int
Just 50

Prelude Control.Monad.Identity> foo 5 :: Either () Int
Right 50

これらのことは、Identity モナドで設計されたプログラムに Maybe 型や Either 型を使ったエラー処理などを簡単に導入できることを示している。

いろいろなモナドのプログラムは、モナドとしての共通の性質に注目すれば、プログラムの本体にほとんど手を入れずに他のモナドのプログラムとして利用できる。これが実際のプログラムにどのような利点をもたらしてくれるのかはわからないが、面白い性質だ。

Haskell のいろいろなプログラムテクニックは発展途中のような気がする。これからも色々な工夫が試され、発展したり、廃れたりしていくのだろう。結構理解するのが大変な割に実際にはあまり有用ではなかったりするものもあるのではないだろうか。しかし、Haskell のそのような様々なトリックを楽しめるところは、いろいろと楽しい。


by tnomura9 | 2018-12-09 23:00 | Haskell | Comments(0)

monad transformers -- モナド変換子とエラー処理(2)

前回、monad transformers step by step 日本語版)の 2 章 3 説で ExceptT モナド変換子を用いたエラー処理を導入した。前回のソースは、複合モナドを導入することでフレームワークを作ったが、エラー処理そのもののコーディングはまだだった。しかし、フレームワークができているので、個別のエラー処理を記述するのは簡単だった。詳しくは原著を参考にしてほしいが、次の実行例のように、構文エラーのときに Left のコンテナにエラーメッセージを入れて表示できる。

*Transformers> runEval2 (eval2 Map.empty (Var "x"))
Identity (Left "unbound variable: x")
*Transformers> runEval2 (eval2 Map.empty (Plus (Lit 1) (Var "x")))
Identity (Left "unbound variable: x")
*Transformers> runEval2 (eval2 Map.empty (App (Lit 1) (Lit 2)))
Identity (Left "type error in application")

モナド変換子と複合モナドの説明がわかりにくかったので今まで挑戦しなかったのが悔やまれる。これまでで、2つのモナドを同時に利用する方法についてなんとなくわかって来たような気がするが、原著にはさらに3つ以上のモナドを含む複合モナドについて解説してある。少々疲れてきがが、すこし休憩してから挑戦してみようと思う。

この記事のソースは次のようになる。

module Transformers where
import Control.Monad.Identity
import Control.Monad.Except
import Control.Monad.Reader
import Control.Monad.State
import Control.Monad.Writer
import Data.Maybe
import qualified Data.Map as Map

type Name = String
data Exp =
    Lit Integer
  | Var Name
  | Plus Exp Exp
  | Abs Name Exp
  | App Exp Exp
  deriving (Show)
data Value =
    IntVal Integer
  | FunVal Env Name Exp
  deriving (Show)
type Env = Map.Map Name Value

exampleExp = Lit 12 `Plus` (App (Abs "x" (Var "x")) (Lit 4 `Plus` Lit 2))

type Eval2 a = ExceptT String Identity a
runEval2 :: Eval2 a -> Identity (Either String a)
runEval2 ev = runExceptT ev

eval2 :: Env -> Exp -> Eval2 Value
eval2 env (Lit i) = return $ IntVal i
eval2 env (Var n) = case Map.lookup n env of
  Nothing -> throwError ("unbound variable: " ++ n)
  Just val -> return val
eval2 env (Plus e1 e2) = do
  e1' <- eval2 env e1
  e2' <- eval2 env e2
  case (e1', e2') of
    (IntVal i1, IntVal i2) -> return $ IntVal (i1 + i2)
    _ -> throwError "type error in addition"
eval2 env (Abs n e) = return $ FunVal env n e
eval2 env (App e1 e2) = do
  val1 <- eval2 env e1
  val2 <- eval2 env e2
  case val1 of
    FunVal env' n body -> eval2 (Map.insert n val2 env') body
    _ -> throwError "type error in application"

by tnomura9 | 2018-12-08 22:59 | Haskell | Comments(0)

monad transformeers -- 複合モナドでエラー処理

今回は Monad Transformers Step by Step (日本語版)の第2章3節に従ってエラー処理を導入する。前回までの Identity モナドのプログラムで Identity モナドの代わりに複合モナド ExceptT String Identity a に変更することで評価プログラムにエラー処理を導入する。おどろいたことに、これは前回の数式の評価をする関数 eval1 には全く変更を行わない。Eval1 a モナドの型とrunEval 関数の定義を変更するだけだ。前回のプログラムでは Eval a モナドの方と runEval1 関数の定義は次のようになっていた。

type Eval1 a = Identity a
runEval1 :: Eval1 a -> a
runEval1 ev = runIdentity ev

これを次のように変えるだけだ。

type Eval2 a = ExceptT String Identity a
runEval2 :: Eval a -> Identity (Either String a)
runEval2 ev = runExceptT ev

元々の Eval1 a は 単に Identity a モナドの別名だった。今回定義した複合モナド Env2 a は、これを ExceptT String Identity a モナドに変更している。複雑だがイメージ的には Except というデータコンストラクタに対し String 型と、Identity モナドと、値 a がコンテナに入っていると思うと分かりやすい(公式の見解ではないあくまでもイメージ)。

runEval2 関数は Eval2 a モナドを引数にとり、Identity (Either String a) 型の値を返す。これはモナド変換子の仕様で、runExcept 関数の戻り値の型が Identity モナドでコンテナの値が Either String a 型の値になることを示している。

イメージ的には、runIdentity 関数は戻り値に Identity a 型の値を返すところが、runEval2 関数では Identity 関数のコンテナの値を Either String a モナドにくるんで返すと考えると分かりやすい。

eval のプログラムはモナドのプログラムなので、Identity モナドであろうと Except String Identity a モナドであろうと一律に使うことができる。使い分けはコンパイラが面倒を見てくれる。これは Identity モナドの関数がそのまま IO モナドで動いてしまうことを見た前回の記事と同じ発想だ。これは、第1章の eval0 関数を第2章のモナドプログラムの eval1 プログラムに編集したときからの目的だ。

以上に述べたたったこれだけの変更で eval1 関数が ExceptT 変換子によるエラー処理付きの評価関数に変貌してしまう。

*Transformers> runEval2 (eval2 Map.empty exampleExp)
Identity (Right (IntVal 18))

動作確認をしたソースは次のようになる。

module Transformers where
import Control.Monad.Identity
import Control.Monad.Except
import Control.Monad.Reader
import Control.Monad.State
import Control.Monad.Writer
import Data.Maybe
import qualified Data.Map as Map

type Name = String
data Exp =
    Lit Integer
  | Var Name
  | Plus Exp Exp
  | Abs Name Exp
  | App Exp Exp
  deriving (Show)
data Value =
    IntVal Integer
  | FunVal Env Name Exp
  deriving (Show)
type Env = Map.Map Name Value

exampleExp = Lit 12 `Plus` (App (Abs "x" (Var "x")) (Lit 4 `Plus` Lit 2))

type Eval2 a = ExceptT String Identity a
runEval2 :: Eval2 a -> Identity (Either String a)
runEval2 ev = runExceptT ev

eval2 :: Env -> Exp -> Eval2 Value
eval2 env (Lit i) = return $ IntVal i
eval2 env (Var n) = return $ fromJust $ Map.lookup n env
eval2 env (Plus e1 e2) = do
  IntVal i1 <- eval2 env e1
  IntVal i2 <- eval2 env e2
  return $ IntVal (i1 + i2)
eval2 env (Abs n e) = return $ FunVal env n e
eval2 env (App e1 e2) = do
  val1 <- eval2 env e1
  val2 <- eval2 env e2
  case val1 of
    FunVal env' n body -> eval2 (Map.insert n val2 env') body

by tnomura9 | 2018-12-08 21:16 | Haskell | Comments(0)

monad transformers -- Identity モナドと IO モナド

前回の記事で式を評価する関数 eval0 をモナド化して eval1 に改装した。eval1 の型は eval1 :: Env -> Exp -> Identity Value で eval1 は Identity モナドの関数だった。しかし、Iidentity モナドで記述された関数はモナド特有の関数として return と >>= しか使っていない。ということは、eval1 関数の記述を全く変更せずに eval1 :: Env -> Exp -> IO Value と eval1 の関数の型を IO モナドのものに変更するだけで eval1 関数は IO モナドの関数として使うことができる。Identity モナドのモナドとしての性質以外全く何もないという性格が、関数をそのまま IO モナドで使うという離れ業を実現させる。

ソースコードは最後に表示するが、以下の実行例のように eval1 の具体的な記述には一切手を入れずに、eval1 を IO モナドの関数として取り扱うことができることがわかる。

コードの main 関数

main = do
  x <- eval1 Map.empty exampleExp
  putStrLn $ show exampleExp
  putStrLn $ show x

実行例

~/programming/Haskell$ runghc evalio.hs
Plus (Lit 12) (App (Abs "x" (Var "x")) (Plus (Lit 4) (Lit 2)))
IntVal 18

ソース

module Transformers where
import Control.Monad.Identity
import Control.Monad.Except
import Control.Monad.Reader
import Control.Monad.State
import Control.Monad.Writer
import Data.Maybe
import qualified Data.Map as Map

type Name = String
data Exp =
    Lit Integer
  | Var Name
  | Plus Exp Exp
  | Abs Name Exp
  | App Exp Exp
  deriving (Show)
data Value =
    IntVal Integer
  | FunVal Env Name Exp
  deriving (Show)
type Env = Map.Map Name Value

exampleExp = Lit 12 `Plus` (App (Abs "x" (Var "x")) (Lit 4 `Plus` Lit 2))

eval1 :: Env -> Exp -> IO Value
eval1 env (Lit i) = return $ IntVal i
eval1 env (Var n) = return $ fromJust $ Map.lookup n env
eval1 env (Plus e1 e2) = do
  IntVal i1 <- eval1 env e1
  IntVal i2 <- eval1 env e2
  return $ IntVal (i1 + i2)
eval1 env (Abs n e) = return $ FunVal env n e
eval1 env (App e1 e2) = do
  val1 <- eval1 env e1
  val2 <- eval1 env e2
  case val1 of
    FunVal env' n body -> eval1 (Map.insert n val2 env') body

main = do
  x <- eval1 Map.empty exampleExp
  putStrLn $ show exampleExp
  putStrLn $ show x

それ自身ではあまり意味がないように思われる Identity モナドだが、Identity モナドで動く関数は、全てのモナドで動作させることができる。自前の関数のモナド化には Identity モナドでの実験が必須とも言える。次回の記事では、これを ExceptT モナドで動くようにする。

by tnomura9 | 2018-12-06 21:25 | Haskell | Comments(0)