Chapter 6. 効果 (effect) を持つプログラミングの機能

Table of Contents
例外
例: ブラウンツリーの判定
参照
例: カウンタの実装
配列
例: 順序をつけた置換 (Ordering Permutations)
行列
例: 定数Piの推量
単純な入力と出力

効果 (effect) を持つプログラミングの機能を使うことで実行時に効果を生成することができます。 ところで効果とは一体何でしょうか? その答はいくぶん込み入っていて、評価モデルに依存しています。 この本では様々な種類の効果を徐々に説明します。 並行ではなく順番に評価がなされるようなプログラムを作る場合、評価に関してある式とその値が見分けがつかない時、その式は効果を持ちません。 例えば、値 3 と見分けがつかないので、式 1+2 は効果を持ちません。 効果を持たない式は純粋であるとも言えます。 他方、効果を持つ式はいかなる値とも区別ができます。 例えば、式 print("Hello") は効果を持ちます。 というのもその評価の結果、観測可能な振舞をひきおこすので、式はいかなる値とも見分けがつくからです。 この例では print("Hello") は I/O に対するある効果を持つと言えます。 もし式の評価が終了しない場合、その式もまた効果を持ちます。 例えば、次のような loop 関数を定義してみましょう:

fun loop (): void = loop ()

すると次のような文脈で、式 loop() はいかなる値とも区別ができます:

let val _ = [] in print ("Terminated") end

この文脈の空欄 []loop() で置き換えると、式の評価は完了しません。 空欄 [] をなんらかの値に置き換えると、その評価は文字列 "Terminated" を印字するでしょう。 式 loop はある非停止性の効果を持つを言えます。

この章では、例外コントロールフローと永続化メモリ記憶、シンプルな I/O に関連するプログラミングの機能を説明します。 これらは実際のプログラミングで一般的に使われるものです。

この章に出てくるコードとテストのための追加コードは オンライン から得られます。

例外

例外機構は、プログラムの評価中に発生した特殊な状態を通知するための効果的な方法です。 しばしばそのような特殊な状態はエラーを指しますが、エラーに関連しない事柄を指す例外を使うこともあります。

ATS では exn 型があらかじめ定義されています。 exn は、新しいコンストラクタによって宣言された拡張データ型であると思うかもしれません。 例えば次のように2つの例外コンストラクタを宣言します:

exception FatalError0 of () exception FatalError1 of (string)

FatalError0 コンストラクタは引数を取らず、FatalError1 コンストラクタは引数を1つ取ります。 例外の値は exn 型の値で、例外コンストラクタを適切な引数に適用することで作られます。 例えば FatalError0()FatalError1("division-by-zero") は2つとも例外の値です (もしくは単に例外と呼ぶこともあります)。 次のプログラムでは、整数の割り算を関数として実装しています:

exception DivisionByZero of () fun divexn (x: int, y: int): int = if y != 0 then x / y else $raise DivisionByZero() // end of [divexn]

関数呼び出し divexn(1, 0) が評価されると、例外 DivisionByZero() が発生します。 ATS における $raise キーワードは例外を発生させます。

なんらかの式 exp が与えられとき ($raise exp) は raise 式です。 式 exp は値を返しますが、($raise exp) を評価すると当然、例外が発生してしまいます。 したがって、raise 式は評価されると値を返しません。つまり raise 式はいかなる型も取りうるのです。

発生した例外は捕捉することができます。 もし例外が捕捉されない場合、発生した例外はプログラムの評価を実行開始した地点で終了させます。 ATS では、try 式 (もしくは try-with 式) は (try exp with clseq) のように作られます。 この式では try キーワード、exp は任意の式、with もキーワード、そして clseq はマッチング節の列です。 try 式を評価すると、最初に exp が評価されます。 もし exp の評価が値を返したら、その値が try 式の値となります。 もし exp の評価が例外を発生させたら、clseq に列挙されたマッチング節のガードに対してその例外をマッチさせます。 もしマッチすれば、発生した例外は捕捉され、マッチしたガード最初の節の中身から評価が続行します。 もしマッチしなかったら、発生した例外は捕捉されません。 try 式ではしばしば with 部は例外ハンドラと呼ばれます。

例外の発生と捕捉をともなう例を見てみましょう。 次のプログラムでは、与えられたリスト中の整数の積を計算する3つの関数が定義されています:

fun listprod1 ( xs: list0 (int) ): int = ( case+ xs of | list0_nil () => 1 | list0_cons (x, xs) => x * listprod1 (xs) ) (* end of [listprod1] *) fun listprod2 ( xs: list0 (int) ) : int = ( case+ xs of | list0_nil () => 1 | list0_cons (x, xs) => if x = 0 then 0 else x * listprod2 (xs) // end of [list0_cons] ) (* end of [listprod2] *) fun listprod3 ( xs: list0 (int) ) : int = let exception ZERO of () fun aux (xs: list0 (int)): int = case+ xs of | list0_cons (x, xs) => if x = 0 then $raise ZERO() else x * aux (xs) | list0_nil () => 1 // end of [aux] in try aux (xs) with ~ZERO () => 0 end // end of [listprod3]

これらの関数は末尾再帰で定義されていますが、ここで主張したいポイントは明確ではありません。 次の事実に疑う余地はないでしょう:

listprod1 関数は通常の作法で定義されていて、先の定理を使っていません。 listprod2 関数は先の定理を部分的に使っています。 試しに listprod2 を7つの整数からなるリストである [1, 2, 3, 0, 4, 5, 6] に対して呼び出してみましょう。 この呼び出しの評価は結局のところ 1*(2*(3*(listprod([0,4,5,6])))) の評価を導きます。 それから 1*(2*(3*0)) が得られ、 1*(2*0) が、さらに 1*0 が、そして最終的に 0 が得られます。 けれども、リスト中に整数 0 を見つけたら、即座に 0 を返すように評価したいと思うでしょう。 このような評価は listprod3 関数であれば実現できます。 listprod3[1, 2, 3, 0, 4, 5, 6] に呼び出した場合、最終的に次の式を評価することになります:

try 1*(2*(3*(aux([0,4,5,6])))) with ~ZERO() => 0

aux([0,4,5,6]) の評価は ZERO() 例外の発生を引き起します。 そしてこの例外は捕捉されて 0listprod3 呼び出しの値として返ります。 with キーワードに続くマッチング節のパターンガードは ~ZERO() であることに注意してください。 別の章でチルダシンボル ~ が必要になる理由を説明します。 ここでは、exn は線形型で、例外の値は線形値であり、その値は消費されるか再度 raise しなければならないということだけ覚えておいてください。 チルダシンボル ~ は、 ~ に続くパターンにマッチした値が消費されることを示しています。 つまり値を保持しているメモリは解放されるということです。

例外は簡単に使いこなせるようなプログラミングの機能ではありません。 また実際、例外の誤用はたくさんあります。 そのため例外を根気強く学び、注意深く使用してください。