2010年1月31日日曜日

Haskell のWriter モナド (1) – Control.Monad.Writer を使う前に

1. 最終的な結果だけでなく、中間結果も出力したい

例えば、次のような簡単な計算を行わせた場合、

(1 + 2) * 3 - 4

答えは `5’  となる。

関数 (演算) が適用される、それぞれの過程の結果を考えると、

1 + 2 => 3

3 * 3 => 9

9 – 4 => 5

となる。

このように最終的な結果だけではなく、計算途中の結果も表示したい場合には、どうすればいいのだろう?

 

2. let 式で中間結果を束縛

まず、考えられる方法は、それぞれの中間結果を、後で参照できるように let 式内で変数に束縛。結果をコンマ区切りで表示するなら、

main = do let a = 1 + 2
              b = a * 3
              c = b - 4
          print $ show a ++ "," ++ show b ++ "," ++ show c ++ ","

結果は、以下のようになる。

"3,9,5,"

 

3. 結果の型を (値, 出力する文字列) に変更

次に、計算の方法を少し変えてみる。

先ほどは、計算過程の結果を「文字列」で返したが、今度は

  • 計算の結果
  • 出力する文字列

の 2 つの部分に分ける。つまり、各計算の結果をタプルで返すように変更。

計算 => (値, 出力する文字列)

main = let (a, log1) = (x, show x) where x = (1 + 2)
           (b, log2) = (y, show y) where y = (a * 3)
           (c, log3) = (z, show z) where z = (b - 4) 
       in  print (c, log1 ++ "," ++ log2 ++ "," ++ log3 ++ ",")

これを実行すると、

(5,"3,9,5,")

ここで出力関数 out を定義する。

out x = (x, show x)

上記を出力関数 out で置き換えると、

main = let (a, log1) = out (1 + 2)
           (b, log2) = out (a * 3)
           (c, log3) = out (b - 4) 
       in  print (c, log1 ++ "," ++ log2 ++ "," ++ log3 ++ ",")

 

4. 式の参照関係に表われるパターン

ここで、let 式内の計算の参照関係について注目する。

下図のように、「上の計算の結果」を「下の計算」が順々に参照していることがわかる。( let 式なので、式の並び順は本質的に意味はない。)

img01-31-2010[1].png

この参照関係は、State モナドについて考えたとき と同じパターン。

  1. 全体の「計算の連なり」の中において、2 つの式の参照関係のみに注目。
  2. 2 つの間の「連なり方」を考えると、式の「つなげ方」を共通の部品として取り出すことができる。
  3. 部品として取り出した「つなぎ」を利用して、全体の連なりを再構成する。

 

5. 2 つの式の「つなぎ方」を定義

では、2 つの式をつなげる関数名を comb と名付け考える。

comb 関数に与える引数を m, n とし、n が m の結果を参照すると考える。先ほどの例では、let 式において out 関数の結果を束縛する部分に相当する。

comb m n = let (a, log1) = m

計算 m の結果をタプル (a, log1) に束縛。 a が「計算の結果」で、変数 log1 は「出力する文字列」に相当。

下図は、「つなぎ」の定義と、先ほどの具体例を比較したところ。

img01-31-2010[2].png

次に、上記の計算結果 a を参照する計算 n について考える。

変数 a を与えられ、計算 n の結果を、タプル (b, log2) に束縛すればいいので、

comb m n = let (a, log1) = m
               (b, log2) = n a

変数 b は、関数適用 n a の「計算の結果」で、log2 は「出力する文字列」に相当。

下図は、「つなぎ」の定義と、先ほどの具体例を比較したところ。

img01-31-2010[4].png

最後に、計算結果 b と、最初の計算 m による出力 log1 と、計算 n による出力 log2 を結合した結果をタプルにつめて返す。

comb m n = let (a, log1) = m
               (b, log2) = n a
           in  (b, log1 ++ log2)

下図は、「つなぎ」の定義と、先ほどの具体例を比較したところ。

img01-31-2010[5].png

具体例では、「つなぎ」を考えずに 3 つの計算を一気に行っている。そのため、「つなぎ」の定義と変数名が異なるが、「つなぎ」は、最終的な結果 c と中間結果として出力したものを結合し、タプルに入れて返すことに相当する。

 

6. 「つなぎ」を使ってみる

上記の comb 関数を使い、先ほどの計算を再定義してみる。

ただし、out 関数においてコンマを出力するように変更した。

comb m n = let (a, log1) = m
               (b, log2) = n a
           in  (b, log1 ++ log2)

out x = (x, show x ++ ",")

main = print $ out (1 + 2)  `comb` \a ->
               out (a * 3)  `comb` \b ->
               out (b - 4)
「つなぎ」を使うメリット

comb 関数を使うことにより、各々の計算の参照関係に対して、気を使う必要がなくなった。どの式が、どの結果に依存するかについて、明示する必要がない。なぜなら、参照関係を comb 関数のつなぎ方に定義してあるため。

ところで、main 関数の定義において、括弧を省略している。これは、無名関数は「できるだけ右へ拡張される」ように解析されるから。

 

7. 前の結果に関心のない「つなぎ」

ここで、一連の本質的な計算とは関わりのない、文字列を出力をするための関数 outStr を定義してみる。

outStr x = ((), x)

本質的な計算の内容に関わらないので、返されるタプルの第1要素は () とする。

この関数を使い、次の計算を実行すると、

main = print $ outStr "begin: " `comb` \_ ->
               out (1 + 2)      `comb` \a ->
               out (a * 3)      `comb` \b ->
               out (b - 4)      `comb` \_->
               outStr " :end"

結果は、以下の通り。

((),"begin: 3,9,5, :end")

あらら… (@_@; 肝心の計算結果であるタプルの第1要素が () になってしまった。 この点は後で修正する。

先に comb 関数を使い、outStr 関数のような本質的な計算に関わらない関数を「つなげる」関数を定義しおく。 (Monad の >> に相当)

comb_ m n = m `comb` \x -> n

comb_ 関数により、上記を書き換えることができる。

main = print $ outStr "begin: " `comb_`
               out (1 + 2)      `comb` \a ->
               out (a * 3)      `comb` \b ->
               out (b - 4)      `comb_`
               outStr " :end"

 

8. 「計算の対象」と「出力する内容」を分離する

上記の「最後で返される値のタプルの第1要素が () となってしまう」問題に対処するために、計算の最後に結果を出力する関数を追加する。

main = print $ outStr "begin: " `comb_`
               out (1 + 2)      `comb` \a ->
               out (a * 3)      `comb` \b ->
               out (b - 4)      `comb` \c ->
               outStr " :end"   `comb_`
               out c

実行すると結果は、

(5,"begin: 3,9,5, :end5,")

計算結果は返ってきたけれど、最後に余分な文字列が出力されてしまった。 (+_+)

これは、out 関数において、次の二つの処理が混在していることが原因。

  1. 本質的な計算の結果を返す
  2. 出力する内容を返す
out x = (x, show x)

また、「本質的な計算の過程の一部を出力したいくない」場合にも対応できない。

例えば、最初の 1+2 の結果を出力せずに計算を進めることは、現在の定義ではできない。 なぜなら、out 関数を使って計算を進め、同時に結果を出力しているため。

この問題を解決するために、out 関数を変更する。これまでは出力する内容と伴に結果を返していたが、出力に特化する

out x = ((), show x ++ ",")

次に、出力は空であるが、結果を返すことに特化した関数を定義する。 (Monad の return に相当)

ret x = (x, "")

この二つの関数を使い、先ほどの関数を定義しなおすと、

main = print $ outStr "begin: " `comb_`
               ret (1 + 2)      `comb` \a -> 
               out a            `comb_`
               ret (a * 3)      `comb` \b ->
               out b            `comb_` 
               ret (b - 4)      `comb` \c ->
               out c            `comb_`
               outStr " :end"   `comb_`
               ret c

全体のソースコードはこちら

 

9. 命令型言語との比較

上記の定義は `comb` に目をつむれば、Python で定義したときとよく似ている。

def main():
    a = 1 + 2
    print a
    b = a * 3
    print b
    c = b - 4
    print c
    return c

print main()

Python で書いた場合と比較すると、

「前に定義した変数を参照する」

ことは、Haskell において

「内側の関数 (無名関数) の中から外側の引数を参照する」

ことに相当することがわかる。

 

参考文献