数学計算のための関数型プログラミング
原文は Matthias Endler により に公開されました。 このブログを購読する
プログラミング言語は、問題に対する汎用的な解法を記述するのに役立つ。その結果がたまたま機械で実行可能になっているに過ぎない。どのプログラミング言語にもそれぞれ異なる長所と短所があり、その理由の一つは、構文と意味論が、その言語で容易に扱える問題の範囲に大きく影響するからだ。
要約: 数学的な計算においては、より一般的な命令型アプローチよりも関数型プログラミングの方が適していると私は考えている。
数学のための組み込み抽象化の活用
言語の背後にある思想(根底にあるプログラミングパラダイム)は、その言語を取り巻くコミュニティの特徴を形作る。開発者たちは、言語コアの周囲に、すぐに使えるライブラリやフレームワークからなる独自のエコシステムを築き上げる。その結果、ビジネスアプリケーションの分野で強みを発揮する言語もあれば(Cobolを思い浮かべる人もいるだろう)、システムプログラミングに優れた言語(CやRustのような)もある。
コンピュータで数学的・数値的な問題を解くとなると、Fortranが思い浮かぶかもしれない。Fortranは汎用言語ではあるが、主に科学技術計算で知られている。もちろん、この言語はその目的のために作られたものだ。だからこそ、Formula Translationという名前が付けられている。
この分野で人気がある理由の一つは、パフォーマンスに配慮しつつ、数学的概念を表現するためのドメイン固有のキーワードが組み込まれていることだ。たとえば、複素数を扱うための専用データ型であるCOMPLEXや、数学用語に近く、配列やベクトルの作成に使えるDIMENSIONというキーワードがある。
命令型スタイルと関数型スタイル
組み込みキーワードは、言語の表現力を特定の問題領域へと広げるのに役立つが、このアプローチには大きな限界がある。言語コアを際限なく拡張していくことは現実的ではない。保守が困難になり、習得にも時間がかかるようになるからだ。そのため、ほとんどの言語は、ルーチンをより小さく管理しやすい部品に分割するための別の抽象化手段――関数やサブルーチン、クラス、オブジェクトなど――を提供している。これらの仕組みはプログラムの複雑さを制御するのに役立つかもしれないが、特に数学的な問題を扱う際には、定型的なコードで解法をわかりにくくしないよう注意が必要だ。
事例 I - 階乗
例として、正の数 n の階乗を計算する次の式をプログラムコードに翻訳することが課題だとしよう。
上記の式を命令型のJavaで実装すると、次のようになるだろう。
public static long fact(final int n) {
if (n < 0) {
// Negative numbers not allowed
return 0;
}
long prod = 1;
for (int i = 1; i <= n; ++i) {
prod *= i;
}
return prod;
}こんな短い問題定義に対して、これはかなり長い解答だ。(なお、1からnまでの明示的なループを使ったバージョンをあえて書いた。再帰関数ならもっと短くなるが、数学的な式にはない概念を使うことになるからだ。)
また、このプログラムにはpublicやstatic、System.err.println()といった言語固有のキーワードが多く含まれている。さらに、プログラマは使用するすべての変数についてデータ型を明示的に指定しなければならず、これは面倒な義務だ。
これらすべてが、数学的な定義をわかりにくくしている。
これを、Haskellのような関数型言語で書かれた次のバージョンと比較してみよう。
fact n = product [1..n]これは問題定義をほぼ直接コードに翻訳したものだ。明示的な型も一時変数も、(publicのような)アクセス修飾子も必要としない。
事例 II - 内積
上記のHaskellプログラムが簡潔なのは、言語がその特定のタスクにぴったりの抽象化(すなわちproductキーワードと[1..n]という範囲構文)を提供しているからだ、と主張する人もいるだろう。そこで、HaskellにもJavaにも標準で備わっていない単純な関数を検討してみよう。2つのベクトルの内積である。数学的な定義は次のとおりだ。
3次元ベクトルの場合、次のように書ける。
まず、Haskellによる実装は次のとおりだ。
type Scalar a = a
data Vector a = Vector a a a deriving (Show)
dot :: (Num a) => Vector a -> Vector a -> Scalar a
(Vector a1 a2 a3) `dot` (Vector b1 b2 b3) = a1*b1 + a2*b2 + a3*b3なお、数学的な型はそれぞれ1行で定義できる。さらに、dot関数は中置記法で定義していることに注目してほしい。つまり、dotの第1引数を関数名の前に、第2引数を後ろに置いているのだ。こうすることで、コードは数学的な表記により近づく。上記の関数の呼び出し例は次のようになる。
(Vector 1 2 3) ’dot’ (Vector 3 2 1)これは短く、正確で、読みやすい。
次に、Javaによる同様の実装を見てみよう。
public static class Vector<T extends Number> {
private T x, y, z;
public Vector(T x, T y, T z) {
this.x = x;
this.y = y;
this.z = z;
}
public double dot(Vector<?> v) {
return (x.doubleValue() * v.x.doubleValue() +
y.doubleValue() * v.y.doubleValue() +
z.doubleValue() * v.z.doubleValue());
}
}
public static void main(String[] args) {
Vector<Integer> a = new Vector<Integer>(3, 2, 1);
Vector<Integer> b = new Vector<Integer>(1, 2, 3);
System.out.println(a.dot(b));
}
}Vectorを適切に文字列表現するには、toString()メソッドもオーバーライドする必要がある。Haskellでは、コードに示したようにShow型クラスからの導出で簡単に済ませられる。
新たな抽象化の創出
関数や型だけでは素直なプログラムを書くのに十分でない場合、Haskellは言語コア自体を拡張する新しい演算子やキーワードを作るためのシンプルな仕組みも提供している。これによりドメイン固有言語が実現可能になり、開発者はプログラミング言語自体の特殊性(メモリ管理や配列の反復など)への対処に回り道することなく、より直接的に本来の問題に取り組むことができる。Haskellはこの考え方を積極的に取り入れているが、Javaにはそのような機能はない。
結論
ここでJavaを貶めたりHaskellを崇拝したりするつもりはない。どちらの言語にもそれぞれの居場所がある。Javaを選んだのは、単に多くのプログラマが読めるからに過ぎない。
比較しているのは、数値計算や記号的プログラミングにおける関数型アプローチと命令型アプローチであり、その点では私はどんなときでも関数型アプローチを好む。余計なものを取り除き、エレガントな解法をもたらしてくれるからだ。高い抽象度で作業し、数学的な言葉で語るための便利な手段を提供してくれる。それにもかかわらず、これらの強みは多くのプログラマに見過ごされている。
エイブラハム・H・マズローの1966年の著書The Psychology of Scienceでの指摘が、ここで当てはまるように思える。
「持っている道具がハンマーだけなら、すべてが釘に見えてしまうのは無理もないのだろう。」
記事をランダムに読む
コメント
ログインしてコメントする