bitterharvest’s diary

A Bitter Harvestは小説の題名。作者は豪州のPeter Yeldham。苦闘の末に勝ちえた偏見からの解放は命との引換になったという悲しい物語

極限-HaskellでのReaderについて

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\)を定義して、さらに理解を深めよう。