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 式なので、式の並び順は本質的に意味はない。)
この参照関係は、State モナドについて考えたとき と同じパターン。
- 全体の「計算の連なり」の中において、2 つの式の参照関係のみに注目。
- 2 つの間の「連なり方」を考えると、式の「つなげ方」を共通の部品として取り出すことができる。
- 部品として取り出した「つなぎ」を利用して、全体の連なりを再構成する。
5. 2 つの式の「つなぎ方」を定義
では、2 つの式をつなげる関数名を comb と名付け考える。
- ( Monad の >>= に相当。 Philip Wadler の Monads for functional programming の “2.9 Variation three, revisited: Output” を真似した。 )
comb 関数に与える引数を m, n とし、n が m の結果を参照すると考える。先ほどの例では、let 式において out 関数の結果を束縛する部分に相当する。
comb m n = let (a, log1) = m
計算 m の結果をタプル (a, log1) に束縛。 a が「計算の結果」で、変数 log1 は「出力する文字列」に相当。
下図は、「つなぎ」の定義と、先ほどの具体例を比較したところ。
次に、上記の計算結果 a を参照する計算 n について考える。
変数 a を与えられ、計算 n の結果を、タプル (b, log2) に束縛すればいいので、
comb m n = let (a, log1) = m (b, log2) = n a
変数 b は、関数適用 n a の「計算の結果」で、log2 は「出力する文字列」に相当。
下図は、「つなぎ」の定義と、先ほどの具体例を比較したところ。
最後に、計算結果 b と、最初の計算 m による出力 log1 と、計算 n による出力 log2 を結合した結果をタプルにつめて返す。
comb m n = let (a, log1) = m (b, log2) = n a in (b, log1 ++ log2)
下図は、「つなぎ」の定義と、先ほどの具体例を比較したところ。
具体例では、「つなぎ」を考えずに 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 関数において、次の二つの処理が混在していることが原因。
- 本質的な計算の結果を返す
- 出力する内容を返す
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 において
「内側の関数 (無名関数) の中から外側の引数を参照する」
ことに相当することがわかる。
参考文献
- Monads for functional programming
- 2.4 Variation three: Output
- 2.9 Variation three, revisited: Output
0コメント:
コメントを投稿