TL;DR

  • Clojure は汎用的なコレクション型を再利用してデータを表すので、再利用性や柔軟性は高いが、明示的な型が不足してコードが理解しづらい傾向にある。
  • Clojure 1.9 で入った spec を使うと、書くコードはそのままで、データ構造や関数の入出力の仕様を明示できる。spec は単なるドキュメントではなく、データ検証やテストの自動生成にも使える。

内容

この章では、次のことを順番に見ていく。

  1. コード内で仕様(spec)を定義し、実行時にそれらを利用する方法
  2. 仕様を組み合わせてデータ検証をする方法
  3. s/fdef を使って、引数のチェックとテストの自動生成をする仕様を定義する方法

Clojure のコードで不足する明示的な型

Java などの静的型付け言語は、プログラム内の各データユニットに対して、ユニークな名前付きデータ構造(クラス)を作る。

一方、Clojure は動的型付け言語であり、プログラムのデータを表現するために汎用的なコレクション型(ベクタやマップ、集合、リスト、シーケンス)を再利用することに依存している。これにより、コードの再利用性や単純さ、汎用性、拡張性が向上する。

しかし、このアプローチだと、プログラマがコードを理解するのに必要な明示的な型が不足し、コードが理解しづらい傾向にある。

Java と Clojure のコードの比較

たとえば、食材(ingredient)のレシピを取り、より大きなレシピのために数量をスケーリングする関数があるとする。

Java の場合、Ingredient と Unit クラスを定義し、メソッドは次のようになる。

public class Ingredient {
	private String name;
	private double quantity;
	private Unit unit;
 
	// ...
 
	public static Ingredient scale(Integredient integredient, double factor) {
		ingredient.setQuantity(ingredient.getQuantity() * factor);
		return ingredient;
	}
}

Clojure の場合だと、食材は単なるマップかもしれない。また、スケーリング関数は明示的な型に言及せずに、マップを受け取り、マップを返すかもしれない。

(defn scale-ingredient [ingredient factor]
	(update ingredient :quantity * factor))

この Clojure のコードは、いくつかの点で Java のコードよりも優れている。

  • ミュータブルな値を直接更新するのではなく、イミュータブルな値を受け取って返すので、理解しやすく、デフォルトでスレッドセーフである
  • マップは簡単に変更できる属性の集合であり、Java クラスの独自の API に閉じ込められることがなく、柔軟性に優れている

一方で、Clojure のコードには ingredient の構造に関する明示的な情報が不足している。プログラマはこの構造をドキュメントやプログラムのコードから推測する必要がある。

問題を解決する spec ライブラリ(Clojure 1.9〜)

Clojure 1.9 で、spec というライブラリが導入された。これにより、データ構造や関数の入出力を記述する仕様(spec)を作れる。

書くコード自体は変わらないが、コードに仕様を追加することで、暗黙的な構造を明示的にできる。たとえば、次のようにコードに仕様を追加できる。

(require '[clojure.spec.alpha :as s])
 
;; Specs describing on ingredient
(s/def ::ingredient (s/keys :req [::name ::quantity ::unit]))
(s/def ::name string?)
(s/def ::quantity number?)
(s/def ::unit keyword?)
 
;; Function spec for scale-ingredient
(s/fdef scale-ingredient
  :args (s/cat :ingredient ::ingredient :factor number?)
  :ret ::ingredient)

このコードは食材(ingredient)を表すマップの形状やフィールド、その内容について正確な説明を提供している。また、関数の spec は scale-ingredient の引数と戻り値を明示的に定義している。

これらの spec は単なるドキュメントではなく、実際のデータ検証や無効なデータの説明、サンプルデータの生成、テストの自動生成など、様々な機能を持っている。

キーワード