第23章 部分操作の処理
この章の目的
この章の終わりまでに、失敗しうる操作、すなわち収まらない算術、ゼロ除算、配列の末尾を越えるインデックス、およびその他いくつかを処理し、プログラムが全域のままで、決して静かに失敗しないようにできるようになります。
全域操作と部分操作
ほとんどの操作は常に値を生成します。2つの小さな数を足すこと、構造体のフィールドを取ること、2つの値を比較すること、これらは全域であり、すべての入力に対して定義されています。いくつかの操作は異なります。それらは数学的に部分的であり、一部の入力で未定義であることを意味します。算術は Word の範囲をオーバーフローしうます。ゼロ除算には答えがありません。インデックスは配列の末尾を越えて指しうます。篩はその値を拒否しうます。これらの各々は、言語が何かをしなければならない実際の入力です。
Keleusma は、部分操作が静かに通り過ぎたりクラッシュしたりすることを許しません。それは各々に定義された結果と、操作を実行してどのケースが起きたかを報告する構文を与え、プログラムが何をするかを決められるようにします。この章では、その構文の一群を扱います。まず算術から始めます。
検査付き算術の構文
その構文は、算術式の後に波括弧で囲まれたアームが続くものです。
fn add_checked(a: Word, b: Word) -> Word {
a + b {
ok(v) => v,
overflow(_, _) => 0,
underflow(_, _) => 0,
}
}
fn main() -> Word {
add_checked(20, 22)
}
keleusma runで実行します。出力は42です。
式 a + b が実行され、その結果がアームのいずれかに振り分けられます。
ok(v)は、真の結果がWordに収まるときに実行されます。結果はvに束縛されます。overflowは、真の結果が大きすぎるときに実行されます。underflowは、真の結果がゼロを大きく下回るときに実行されます。
20 + 22 の場合、結果 42 は収まるので、ok アームが実行されます。
結果が収まらないとき
最大の Word にもう1を足すように main を変更してください。
fn main() -> Word {
add_checked(9223372036854775807, 1)
}
その和は最大の Word を1つ越えます。今度は代わりに overflow アームが実行され、関数は 0 を返します。算術は静かに失敗せず、静かに誤った答えを生成することもありませんでした。構文はオーバーフローを報告し、プログラムがそれについて何をするかを決めました。
上位半分と下位半分
上記では overflow と underflow のアームを overflow(_, _) と書き、これらが保持する値を無視していました。Word においてこれらは二つの値、すなわち真の結果の上位半分と下位半分を保持します。これらは Word の二倍の幅を持つ数の中で計算されます。
overflow(high, low) => ...
これら二つの半分は多倍長算術の基礎です。一つの Word には大きすぎる数は、上位半分と下位半分という一対として保持され、ある位からの桁上げが次の位へと連なっていきます。同梱された例 examples/scripts/09_big_numbers.kel とガイドページ BIG_NUMBERS.md は、この技法を余すところなく取り扱っています。
第一級の多倍長型
一般的な場合には、桁上げを手作業で連ねる必要はありません。Multiword<N, F> 型は固定幅の多倍長固定小数点値であり、N ワード幅で F 個の小数ビットを持ち、半分の値を代わりに保持してくれます。Multiword<N> という形式は整数の場合であり、Multiword<N, 0> と等価です。ワードのタプルから最下位を先頭にして構築し、そのワードをインデックスで取り出します。
fn main() -> Word {
let a = (9223372036854775807, 0) as Multiword<2>;
let b = (1, 0) as Multiword<2>;
let s = a + b;
s[1]
}
a の下位ワードは最大の Word です。1 を加えるとそのワードの最上位ビットが立ち、最小の Word へと変わりますが、下位ワードから桁上げは出ないため、上位ワード s[1] は 0 のままです。これは正しい符号なし多倍長の桁上げであり、上記の検査付き構文における符号付きオーバーフロー報告とは異なります。加算、減算、および六つの比較は、本章で説明する桁上げおよび桁借りのカスケードそのものへと下位変換されるため、これらの演算は新たな命令を追加しません。小数スケール F を適用する整数および固定小数点の乗算、除算、剰余に加えて、四つのシフト lsl、asl、lsr、asr、およびリムごとのビット単位演算子 band、bor、bxor、bnot も利用可能です。この型は B19 として提供されました。
省略可能なアームとラップアラウンドの既定動作
overflow と underflow のアームは省略可能です。これらを省略すると、範囲外の結果は二の補数でラップアラウンドし、素のマシン算術と同じ動作になります。したがって ok アームだけを持つ構文は、まさに通常のラップアラウンド演算であり、意図が見えるように書き出したものです。
let total = a + b { ok(v) => v };
扱いたい場合についてのみアームを追加します。ok アームは常に必ず書かなければならないものです。
ゼロ除算
除算と剰余には別の失敗、すなわちゼロの除数があり、この場合は結果がまったく存在しません。zero_divisor アームがこれを扱い、被除数を束縛します。
fn safe_div(a: Word, b: Word) -> Word {
a / b {
ok(q) => q,
zero_divisor(n) => 0,
}
}
fn main() -> Word {
safe_div(10, 0)
}
出力は 0 です。zero_divisor アームがなければ、ゼロによる除算は暗黙のうちに誤った答えを生成するのではなく、回復可能なエラーによってプログラムを停止します。
その他の数値型
この構文は Word だけでなく四つの数値型で機能します。Byte、Float、Fixed<N> においては、オーバーフローまたはアンダーフローのアームは二つの半分ではなく単一の結果を束縛します。これらの型は多倍長の上位半分を保持しないためです。
fn main() -> Byte {
200Byte + 100Byte {
ok(v) => v,
overflow(w) => w,
}
}
合計 300 は Byte に収まらないため、overflow アームが実行され、ラップされた結果 w、すなわち 44 を束縛します。Byte の結果はその型でタグ付けされた値として Byte(44) と出力されます。サポートされる演算子は +、-、*、/、%、算術左シフト asl、および単項の - であり、許容されるアームは型に依存します。たとえば符号なしの Byte は加算でオーバーフローし得ますが、減算でのみゼロを下回り得ます。算術左シフト asl は値 x * 2^k であるため、Word においては乗算とまったく同様にオーバーフローまたはゼロ以下になり得て、同じ overflow および underflow のアームを取ります。
端への飽和
アーム本体の内部では、キーワード saturate_max と saturate_min が構文の型の最大値および最小値を表します。これらは、手作業で数を選ぶのではなく、範囲外の結果を範囲の端へとクランプすることを可能にします。
fn main() -> Byte {
200Byte + 100Byte {
ok(v) => v,
overflow(_) => saturate_max,
}
}
出力は最大の Byte である Byte(255) です。Word においてこれらのキーワードはワードの境界であり、Float においては最大値と最も負の有限値、Fixed<N> においては固定小数点の極値です。結果の型が with saturate_max または with saturate_min の値を宣言した精緻化されたニュータイプである場合、キーワードはその宣言された境界へと解決されます。
構文の一族
同じ波括弧とアームの形式が、言語におけるあらゆる部分演算を、それぞれ固有のアームキーワードとともに扱います。
インデックス指定。 配列のインデックスは末尾を超えて指し示し得ます。invalid_index アームは問題のあるインデックスを束縛し、ok は要素を束縛します。
fn main() -> Word {
let a = [10, 20, 30];
a[9] {
ok(v) => v,
invalid_index(_) => 0,
}
}
インデックス 9 は範囲外であるため、結果は 0 です。
ニュータイプの構築。 精緻化されたニュータイプの構築は、値が規則に反する場合に失敗し得ます。invalid_newtype アームは述語が拒否した値を束縛します。
fn is_positive(x: Word) -> bool { x > 0 }
newtype Positive = Word where is_positive;
fn main() -> Word {
let p = Positive(0 - 4) {
ok(v) => v as Word,
invalid_newtype(_) => 1,
};
p
}
値 -4 は規則に不合格となるため、invalid_newtype アームが実行され、結果は 1 です。
判別子から列挙型へ。 Word は列挙型の値へと戻すことができます。これは列挙型をその判別子へキャストする逆の操作です。ユニットバリアントはそれ自身へと変換され、payload_discriminant アームはペイロードを持つバリアントのペイロードを供給し、invalid_discriminant はどのバリアントにも一致しない Word を捕捉します。
enum Signal { Stop = 0, Go = 1 }
fn main() -> Word {
let s = 1 as Signal {
invalid_discriminant(_) => Signal::Stop,
};
s as Word
}
判別子 1 は Go バリアントであるため、結果は 1 です。
ネイティブ呼び出し。 ホストが提供するネイティブ関数は失敗を報告し得ます。error アームはネイティブが報告する Word エラーコードを束縛し、ok は成功値を束縛します。
let row = host::lookup(id) {
ok(v) => v,
error(code) => code,
};
ネイティブ呼び出しは keleusma run からではなく、埋め込みホストから実行されます。第33章、ネイティブの登録 は、ホストがエラーコードを報告する方法を含めて、ホスト側を示します。
二つのバックエンド、一つの契約
ここに示すすべての構文は一つの契約を共有します。バイトコード仮想マシン、すなわち keleusma run で実行する検証付きインタプリタは、未処理の部分演算のいずれに対してもトラップします。トラップはホストが受け取る回復可能なエラーであり、クラッシュではありません。同じプログラムの将来のネイティブビルド(後のマイルストーンの主題です)は、代わりに定義された、クラッシュしない値を生成します。ハードウェアがフォールトしない場合はハードウェアの結果を用い、フォールトする場合は挿入された小さなチェックを用います。二つのビルドは、あなたが処理しなかった部分演算においてのみ差異が生じ得ます。これらの構文を通じてすべての結果を処理すれば、あなたのプログラムは全域的です。すなわち両方のバックエンドで同じ結果を生成し、決してトラップしません。各バックエンドが各演算に対して生成する値を含む完全な契約は、RUNTIME_FAULTS.md に規定されています。
この章のまとめ
- いくつかの演算は部分的であり、一部の入力に対して未定義です。この言語はそれぞれに定義された結果と、それを処理するための構文を与えます。
- 検査付き算術
a + b { ok(v) => ..., overflow(...) => ... }はオーバーフロー、アンダーフロー、およびゼロの除数を報告します。overflowとunderflowのアームは省略可能であり、既定ではラップアラウンドします。okは必須です。Wordにおいてアームは上位半分と下位半分を保持し、これが多倍長算術の基礎となります。Byte、Float、Fixed<N>においては単一の結果を保持します。 saturate_maxとsaturate_minは型の範囲の端へとクランプします。- 同じ形式がインデックス指定(
invalid_index)、ニュータイプの構築(invalid_newtype)、判別子から列挙型への変換(payload_discriminant、invalid_discriminant)、およびネイティブ呼び出し(error)を扱います。 - 処理されていない部分演算は仮想マシン上でトラップします。あらゆる結果を処理することがプログラムを全域的にします。
第VI部の最後となる次章では、データを機密として印付けし、それがどこへ流れるかを言語に追跡させます。