3.Haskellでの極限と余極限
これまでの記事で、極限と余極限の説明をしてきた。数学的な記述が主で、Haskellを学ぼうとしている人は、役に立たないなと感じたことだろう。圏論での積や余積は、乗算や加算、あるいは、論理積や論理和と関係があることは、直感でもわかる。しかし、これらのために、わざわざ圏論まで持ち出す必要はないと思われただろう。
そこで、今回は、日常的に使われるることはないが、Haskellの奥深さだけではなく、両者の繋がりを巧みに伝えてくれるHaskellの秘密兵器を紹介しよう。
3.1 \(Reader\)を理解しよう
今回、紹介する話題は\(Reader\)である。これまでも説明してきたので、理解されている方も多いと思うが、初めてという方もいることと思う。
\(Reader\)は、なかなか理解しにくいので、手始めにどのような機能を有していて、どのように利用されているかについて説明しよう。
Haskellでは\(Reader\)は次のように定義されている。
type Reader u = ReaderT * u Identity
この定義だと、\(ReaderT\)を理解することが必要になるので、手っ取り早く\(Reader\)を理解することはできない。そこで、上記の定義をとりあえず置いておき、次のように理解しておこう。
type Reader u a
ここで、\(u\)は環境と呼ばれるもので、\(a\)は環境から作り出される値である。少し、利用してみよう。
\(Reader\)には、\(return\)という関数が用意されているので、これを用いて\(Reader\)の値を作り出してみよう。
Prelude> import Control.Monad.Reader Prelude Control.Monad.Reader> a = return 3.5 :: Reader String Double
それでは、\(return\)によって戻された値の型を確認しておこう。
Prelude Control.Monad.Reader> :t a a :: Reader String Double
他にもいくつか試してみよう。
Prelude Control.Monad.Reader> b = return "A Happy New Year." :: Reader String String Prelude Control.Monad.Reader> :t b b :: Reader String String Prelude Control.Monad.Reader> c = return 3 :: Reader Int Int Prelude Control.Monad.Reader> :t c c :: Reader Int Int
\(Reader\)には、\(runReader\)という関数も用意されている。この関数は、\(Reader\)の値と、環境の値とを入力すると、\(Reader\)の2番目の型変数に対応した値を出力してくれる。まずは実行例を示そう。
Prelude Control.Monad.Reader> a = return 3.5 :: Reader String Double Prelude Control.Monad.Reader> d = runReader a "Please, execute it." Prelude Control.Monad.Reader> d 3.5 Prelude Control.Monad.Reader> :t d d :: Double
当然、環境の値のデータ型が異なると実行されない。
Prelude Control.Monad.Reader> runReader a 3 <interactive>:22:13: error: ? No instance for (Num String) arising from the literal ‘3’ ? In the second argument of ‘runReader’, namely ‘3’ In the expression: runReader a 3 In an equation for ‘it’: it = runReader a 3
\(runReader\)の型シグネチャは次のようになっている。
runReader :: Reader u a -> u -> a
1) 預金残高と利息
\(Reader\)の性格が分かってきたところで、一般的な使い方を示そう。これまで説明しなかったが、\(Reader\)はモナドである。従って、通常のプログラミング言語と同じように、逐次的に命令の列を用意することが可能である。多く用いられているのは、環境を入力して、それに基づいて出力するというものである。
環境を入力する関数として\(ask\)が用意されている。これを利用していくつかの例を示そう。最初の例は、年利率で2%、10万円の定期預金をした時に、\(r\)年後の残高を求めることにしよう。この例では、\(r\)が環境であり、残高が出力である。
これは次のようなプログラムになる。
import Control.Monad.Reader amount :: Reader Int Double amount = do year <- ask -- 何年間預金したかを入力する return (100000 * 1.02 ** (fromIntegral year)) --10万円預金したときの残高を出力
このプログラムを実行してみる。3年後の残高を求めてみよう。
Prelude> :load "amount.hs" [1 of 1] Compiling Main ( amount.hs, interpreted ) Ok, modules loaded: Main. *Main> runReader amount 3 106120.79999999999
3年後には、6千円以上の利息が付いていることが分かる。今の銀行預金の利率もこのくらいならば、預金しがいもあるというものだ。
それでは、利率が3%になった時の比較をしてみよう。プログラムは次のようになる。
import Control.Monad.Reader amount2 :: Reader Int Double amount2 = do year <- ask -- 何年間預金したかを入力する return (100000 * 1.02 ** (fromIntegral year)) --2%の年率で10万円預金したときの残高を出力 amount3 :: Reader Int Double amount3 = do year <- ask -- 何年間預金したかを入力する return (100000 * 1.03 ** (fromIntegral year)) --3%の年率で10万円預金したときの残高を出力 amount2vsAmount3 :: Reader Int String amount2vsAmount3 = do a2 <- amount2 --2%の年率を実行 a3 <- amount3 --3%の年率を実行 return ( "2%: " ++ show a2 ++ " vs " ++ "3%: " ++ show a3)
上のプログラムに見るように、比較のプログラム\(amount2vsAmout3\)は、2%と3%のプログラム\(amoun2\)と\(amount3\)を実行させるだけだ。
さて、これを用いて4年間預けたときでの比較をしてみよう。
runReader amount2vsAmount3 4 "2%: 108243.216 vs 3%: 112550.881"
3%にはさらに魅力を感じることだろう。4年間預けておけば、利息だけで小旅行ができそうだ。
2)米国からドイツへ移動したときの単位の変換
\(Reader\)の使い方に慣れてきたが、さらに、ダメ押しでもう一つ例を上げてみよう。今度は、米国からドイツへ移動したときの単位の変換だ。
まず、変換のためのデータ型\(Conv\)を新たに用意しておこう。次のようにした。
data Conv = Conv
次にマイルからキロメートルへの変換のプログラムを作ろう。1マイルは1.6㎞なので次のようにする。
m2k :: Reader Conv String m2k = do env <- ask return "One mile is equal to 1.6 km."
同様にポンドからキログラムへの変換プログラムを作ろう。1ポンドは0.45kgであるので、次のようになる。
p2k :: Reader Conv String p2k = do env <- ask return "One pound is equal to 0.45 kg."
さらに通貨を変換してくれるプログラムを作ろう。1ドルは0.83ユーロとすると、次のようになる。
d2e :: Reader Conv String d2e = do env <- ask return "One Dollar is equal to 0.83EUR."
最後にすべての変換を与えてくれるプログラムを作ろう。これは上記のプログラムを用いて次のようになる。
u2g :: Reader Conv String u2g = do length <- m2k weight <- p2k money <- d2e return (length ++ " \n " ++ weight ++ " \n " ++ money)
それではこれを実行してみよう。
Prelude> :load "Reader.hs" [1 of 1] Compiling Main ( Reader.hs, interpreted ) Ok, modules loaded: Main. *Main> runReader u2g Conv "One mile is equal to 1.6 km. \n One pound is equal to 0.45 kg. \n One Dollar is equal to 0.83EUR."
このプログラムは、\(u2g\)というファイルを読みだしているようには見えないだろうか。このように、\(Reader\)には、データベースやファイルを読みだしているように感じさせる機能がある。
次回は、\(Reader\)を定義して、さらに理解を深めよう。