数値型・関数・小さいテスト
数値計算では、まず小さな関数を正しく書き、それを手計算できる入力で検証することが重要です。本節では、以降の章で繰り返し使う最小限のRustの形を確認します。
なぜ小さく分けてテストするか
数値計算プログラムは、1回だけ数値を出せば正しい、というものではありません。 間違いを見つけ、原因を局所化し、修正できる形にしておく必要があります。
これは車を組み立てる作業に似ています。 車にはエンジン、ブレーキ、車輪、配線など多くの部品があります。 すべてを組み上げてから最後に走行テストだけを行うと、 不具合があることは分かっても、どの部品が原因かはすぐには分かりません。 実際には、部品ごとに検査し、いくつかの部品を組み合わせた段階でも検査し、 最後に全体を検査します。
ソフトウェアでも同じです。 小さい関数に分けて単体テストを書けば、計算の部品を個別に検査できます。 他の言語へ移植する場合も、プログラム全体を一度に移すのではなく、 部品ごとに移植し、同じ入力と期待値で正しさを確認できます。
テストを最初から導入することには、もう1つ重要な効果があります。
テストしやすい形にするためには、入力と出力が明確な関数に分ける必要があります。
つまり、早い段階でテストを書くと、自然に部品の境界が分かれます。
逆に、長いmain関数や隠れたグローバル状態に依存する形で書いた後から
テストを足すのは困難です。
数値型
本書では、特に断りがない限り実数にはf64を使います。配列の添字や要素数にはusizeを使います。
let dt: f64 = 0.01;
let n_steps: usize = 1000;
let t = n_steps as f64 * dt;
浮動小数点数は、数学的な実数そのものではなく、有限のビット数で実数を近似した値です。 丸め誤差や比較の注意点については、 次節「浮動小数点演算と誤差」で扱います。
整数と浮動小数点数は自動では混ざりません。必要な場所で明示的に変換します。これは冗長に見えることもありますが、数値計算では、添字なのか物理量なのかを読み分けやすくする効果があります。
関数として切り出す
数値計算コードは、最初から大きなmain関数に書くのではなく、小さい関数に分けます。例えば、自由落下の位置を計算する関数は次のように書けます。
fn height(t: f64, h0: f64, v0: f64, g: f64) -> f64 {
h0 + v0 * t - 0.5 * g * t * t
}
この関数は、入力と出力が明確です。ファイル入出力、プロット、コマンドライン引数とは独立しているため、単体テストしやすくなります。
traitの最小限の見方
trait(トレイト) は、ある型が「何をできるか」を表す約束です。
本書では、最初から複雑なジェネリクスを書く必要はありません。
まずは f64 や Vec<f64> のような具体的な型で関数を書き、必要になったときに
trait を読みます。
数値計算でよく見る trait には、次のようなものがあります。
Debug:{:?}で表示できる。Copy: 代入や関数呼び出しで値をコピーできる。Add,Sub,Mul,Div:+,-,*,/の演算に対応する。- 外部crateのtrait: そのcrateが提供するメソッドを型に追加する。
例えば、後の章で出てくる ndarray-linalg では、
Norm や Solve のような trait を use することで、
配列にノルム計算や連立方程式を解くメソッドが見えるようになります。
このような use は、単に名前を短くするだけでなく、
trait が提供するメソッドを使うために必要な場合があります。
標準ライブラリには、f32 と f64 をまとめる Float trait はありません。
この教材では、特に理由がなければ f64 で書きます。
複数の浮動小数点型に対応する必要が出てきた場合だけ、num-traits のような
外部crateを検討します。
次の例は、trait bound を使った小さい関数です。 このような書き方は、関数を複数の数値型で使いたいときにだけ検討します。
use std::ops::{Add, Mul};
fn squared_norm2<T>(x: T, y: T) -> T
where
T: Copy + Add<Output = T> + Mul<Output = T>,
{
x * x + y * y
}
ここで T: Copy + Add<Output = T> + Mul<Output = T> は、
T がコピーでき、足し算と掛け算の結果も同じ T になることを要求しています。
ただし、最初からすべてを generic にする必要はありません。
物理量や配列の型が決まっている場合は、具体的な f64 の関数の方が読みやすいことも多いです。
小さいテスト
浮動小数点数では、==による厳密比較を避け、許容誤差を使って比較します。
fn height(t: f64, h0: f64, v0: f64, g: f64) -> f64 {
h0 + v0 * t - 0.5 * g * t * t
}
fn close(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
#[test]
fn height_at_initial_time_is_initial_height() {
let h = height(0.0, 100.0, 0.0, 9.8);
assert!(close(h, 100.0, 1e-12));
}
テストは、まず手計算できる小さい入力から始めます。解析解、保存量、極限ケース、対称性などがある場合は、それをテストに使います。
実行と検査
テストは次のコマンドで実行します。
cargo test
AI agent に実装を任せる場合も、少なくとも次の確認を行います。
cargo check
cargo test
git diff
git diff HEAD
cargo checkやcargo testは、型、借用、単体テストを検査します。
git diffはまだstageしていない変更を表示し、
git diff HEADは最終commitから現在の作業ツリーまでの変更全体を表示します。
ただし、コンパイルが通ることは、物理モデルや数値計算法が正しいことを意味しません。
人間は、問題設定、単位、境界条件、検証方法を確認します。