はじめに
OCamlのモジュールシステムとCommon LispのCLOSは、どちらもプログラムを構造化し、再利用性を高めるための強力な仕組みです。しかし、そのアプローチは大きく異なります。今回は、図形を描画するという同じ具体例を使って、それぞれの違いを見ていこうと思います。
図形を「部品」として扱うOCamlのモジュールシステム
OCamlのモジュールは、関連するデータ型と関数を一つのカプセルにまとめることで、プログラムの整理と安全性を高めます。図形を描画する例では、Shape
というモジュールを定義し、その中に図形のデータ型と、それに対応する描画関数を格納します。
まず、図形を表すデータ型を定義します。これはバリアント型で表現します。
type shape =
| Circle of {radius: float}
| Square of {side: float}
module Shape = struct
type t = shape
let draw (s: t) =
match s with
| Circle {radius} ->
Printf.printf "円を描画します。半径は %f です。\n" radius
| Square {side} ->
Printf.printf "四角形を描画します。一辺の長さは %f です。\n" side
end
let my_cicle = Circle {radius = 5.0}
let my_square = Square {side = 10.0}
let () =
Shape.draw my_cicle;
Shape.draw my_square;
このコードでは、Shape
モジュールの中にdraw
関数を定義し、その関数がパターンマッチを使って異なる図形(CircleとSquare)を判別し、適切な処理を実行しています。この方法は、型システムによってコンパイル時に安全性が保証されるため、予期せぬエラーを防ぐことができます。
図形を「振る舞い」で扱うCommon LispのCLOS
CLOSは、オブジェクト指向の概念を使って、データ(クラス)と振る舞い(メソッド)を結びつけます。図形を描画する例では、draw
というジェネリック関数を定義し、異なる図形クラス(circleとsquare)ごとに、その描画方法を定義します。
まず、共通の基底クラスshape
と具体的な図形クラスcircle
とsquare
を定義します。
(defclass shape () ())
(defclass circle (shape)
((radius :initarg :radius
:accessor radius)))
(defclass square (shape)
((side :initarg :side
:accessor side)))
(defgeneric draw (shape))
(defmethod draw ((c circle))
(format t "円を描画します。半径は ~aです。~%" (radius c)))
(defmethod draw ((s square))
(format t "四角形を描画します。一辺の長さは ~aです。~%" (side s)))
(let ((my-circle (make-instance 'circle :radius 5))
(my-square (make-instance 'square :side 10)))
(draw my-circle)
(draw my-square))
上記のコードで、基底クラスを定義せず、以下のようにすることも可能です。
(defclass circle ()
((radius :initarg :radius
:accessor radius)))
(defclass square ()
((side :initarg :side
:accessor side)))
(defgeneric draw (shape))
(defmethod draw ((shape circle))
(format t "円を描画します。半径は ~aです。~%" (radius shape)))
(defmethod draw ((shape square))
(format t "四角形を描画します。一辺の長さは ~aです。~%" (side shape)))
しかし、基底クラスshape
を定義して、circle
とsquare
はshape
のサブクラスであることを明示しておくことで可読性が向上し、draw
関数がshape
クラスのオブジェクトのための関数であることが分かりやすいように思います。
この仕組みにより、例えばtriangle
というような新しい図形を追加する場合、新しいメソッドを追加するだけで対応できます。
まとめ
OCamlのモジュールシステムは、データと関数を一つの部品としてまとめ、プログラム全体を静的に構造化することに優れています。コンパイル時のチェックにより、堅牢なプログラムを構築できます。
一方、Common LispのCLOSは、データ(オブジェクト)と振る舞い(メソッド)を分離し、実行時の柔軟な振る舞いを可能にします。動的なプログラムや、複雑なシステムを拡張していく際に非常に強力です。
それぞれの言語が持つ思想が、これらの異なるアプローチに表れています。どちらも学ぶ価値のある概念ですね。