def チュートリアル
このチュートリアルでは、floating のすべての数値型で一度に動作するコードの書き方を示します。Floating トレイトを通じて値を調べ、ジェネリックな述語でそのクラスを判定し、IEEE の比較結果を PartialOrder として読み取り、def が再エクスポートする算術のコンテキスト型とエラー型を再利用します。def 自身は算術を行わず、それを行うのは具体的なパッケージ(bin_float、decimal、decimal_gda、ball_float)です。トレイトの背後にある法則は設計ページにあります。
クイックスタート
モジュールを追加し、使用する表現と並べて def をインポートします。
moon add Luna-Flow/floating@0.8.0
import {
"Luna-Flow/floating/def",
"Luna-Flow/floating/bin_float",
}
最小限の有用なプログラムは、値にそれが何であるかを尋ねます。
///|
test "quick start: observe a value" {
let x = @bin_float.BinFloat::from_double(-2.5)
inspect(@def.is_finite(x), content="true")
inspect(@def.Floating::sign(x) == @def.Sign::Negative, content="true")
inspect(@def.Floating::precision(x), content="53")
}
日常的なタスク
すべての表現に対して一つの関数を書く
F : @def.Floating で制約された関数は、二進、IEEE 十進、GDA 十進、区間の値を受け付けます。MoonBit はもはやトレイトメソッドを型パラメータのドットメソッドにしないので、トレイトメソッドは修飾形式 @def.Floating::classify(x) で呼び出してください。
///|
fn[F : @def.Floating] summary(x : F) -> String {
if @def.is_nan(x) {
return "nan"
}
let sign = match @def.Floating::sign(x) {
Negative => "negative"
Zero => "zero"
Positive => "positive"
}
let kind = if @def.is_infinite(x) { "infinite" } else { "finite" }
"\{kind} \{sign} at precision \{@def.Floating::precision(x)}"
}
///|
test "one summary for four representations" {
inspect(
summary(@bin_float.BinFloat::from_int(7)),
content="finite positive at precision 53",
)
inspect(
summary(@decimal.Decimal::from_string("-0.00").unwrap()),
content="finite zero at precision 34",
)
inspect(
summary(@decimal_gda.Decimal::from_string("-Infinity").unwrap()),
content="infinite negative at precision 34",
)
inspect(summary(@ball_float.BallFloat::empty()), content="nan")
}
区間の場合は、なぜ NaN の判定を先に行うのかを示しています。空区間は NaN に分類され、BallFloat::sign はそれに対して異常終了します。
精度をジェネリックに変更する
with_precision は、値自身の基数での指定された有効桁数、つまり二進ではビット数、十進では桁数に丸めます。
///|
fn[F : @def.Floating] three_digits(x : F) -> F {
@def.Floating::with_precision(x, 3, @lf_arith.RoundingMode::ToNearestEven)
}
///|
test "round to three significant digits of the radix" {
let d = three_digits(@decimal.Decimal::from_string("3.14159").unwrap())
inspect(d.to_string(), content="3.14")
let b = three_digits(@bin_float.BinFloat::from_int(11))
inspect(b.to_string(), content="3p2")
}
11 は二進で 1011 です。有効ビット 3 桁に丸めると 1100 になり、to_string はそれを として出力します。
IEEE の比較を読み取る
IEEE 754 に従う比較は PartialOrder を返し、その四番目の値 Unordered は NaN のオペランドを示します。
///|
fn relation(a : @bin_float.BinFloat, b : @bin_float.BinFloat) -> String {
match a.compare_quiet(b).0 {
Less => "less"
Equal => "equal"
Greater => "greater"
Unordered => "unordered"
}
}
///|
test "four-way comparison" {
let one = @bin_float.BinFloat::from_int(1)
let two = @bin_float.BinFloat::from_int(2)
inspect(relation(one, two), content="less")
inspect(relation(@bin_float.BinFloat::nan(), @bin_float.BinFloat::nan()), content="unordered")
inspect(
relation(@bin_float.BinFloat::from_double(-0.0), @bin_float.BinFloat::zero()),
content="equal",
)
}
表現を比較する前に正規化する
同じ値を持つ十進数でも、格納されている指数が異なることがあります(1.5 と 1.500 は一つのコホートに属します)。normalized は標準的な元を選ぶので、出力される形が一致します。
///|
test "normalize a decimal cohort" {
let a = @decimal.Decimal::from_string("1.500").unwrap()
let b = @decimal.Decimal::from_string("1.5").unwrap()
inspect(a.to_string(), content="1.500")
inspect(
@def.Floating::normalized(a).to_string() ==
@def.Floating::normalized(b).to_string(),
content="true",
)
}
さらに進んで
再エクスポートされた算術型を使う
@def.ArithmeticContext、@def.ArithmeticError、@def.RoundingMode およびその他のエイリアスは Luna-Flow/arithmetic の型です。語彙だけが必要なコードは、arithmetic の代わりに def をインポートできます。
///|
fn describe_error(e : @def.ArithmeticError) -> String {
if e.is_division_by_zero() {
"division by zero: " + e.message
} else if e.is_domain_error() {
"domain error: " + e.message
} else {
"other: " + e.message
}
}
///|
test "handle a checked result through def" {
let result = @bin_float.BinFloat::from_int(1).div_checked(
@bin_float.BinFloat::zero(),
)
match result {
Ok(_) => fail("expected an error")
Err(e) => inspect(describe_error(e), content="division by zero: division by zero")
}
}
独自の型に Floating を実装する
トレイトは開いています。ラッパー型は既存の実装に委譲できますが、その場合も設計ページに挙げた法則を守らなければなりません(例えば normalized は値を変えてはいけません)。
///|
struct Measured {
value : @bin_float.BinFloat
unit : String
}
///|
impl @def.Floating for Measured with classify(self) {
@def.Floating::classify(self.value)
}
///|
impl @def.Floating for Measured with sign(self) {
@def.Floating::sign(self.value)
}
///|
impl @def.Floating for Measured with precision(self) {
@def.Floating::precision(self.value)
}
///|
impl @def.Floating for Measured with with_precision(self, precision, mode) {
{ ..self, value: @def.Floating::with_precision(self.value, precision, mode) }
}
///|
impl @def.Floating for Measured with normalized(self) {
{ ..self, value: @def.Floating::normalized(self.value) }
}
///|
test "a user type joins the generic code" {
let m = { value: @bin_float.BinFloat::from_int(-4), unit: "m" }
inspect(summary(m), content="finite negative at precision 53")
inspect(@def.is_zero(m), content="false")
let coarse = @def.Floating::with_precision(m, 1, @lf_arith.RoundingMode::TowardZero)
inspect(coarse.unit, content="m")
}
よくある落とし穴
Sign::Zeroは「値がゼロである」ことを意味しません。スカラー型では NaN に対しても、またゼロを含むあらゆる区間に対しても返されます。先に@def.is_nanを判定し、区間ではBallFloat::contains_zeroか上下界を使ってください。BallFloatに対する@def.is_zeroは で真になります。この述語はsignを通じて定義されており、区間ではそれが「ゼロを含む」を意味するからです。Signは と を区別できません。符号ビットが重要な場合は具体的なパッケージ(BinFloat::is_negative_zero、Decimal::is_signed)を使ってください。PartialOrderはソートに使える順序ではありません。Unorderedが三分律を破るからです。ソートには具体的なパッケージの全順序(BinFloat::total_order、Decimal::compare_total)を使ってください。with_precisionはフラグを報告せず、指数の限界も無視します。丸めを観測する必要がある場合は、具体的なパッケージの*_ctx演算を使ってください。with_precision(x, 0, mode)はエラーではありません。精度は 1 に切り上げられます。
次のステップ
defの設計: トレイトの法則とPartialOrderの順序理論。defAPI: すべての項目とそのシグネチャ。semanticチュートリアル: 異なる表現の値を厳密に比較する。- リポジトリ共通の語彙については数値意味論ガイドを参照してください。