型システムと静的型付け言語/動的型付け言語
型システムは、コンパイル時または実行時に型検査を実行して、型の不一致がある場合にエラーを報告するものであり、プログラミング言語において、型システムはプログラム内で使用される値の種類を記述し、値の種類の不一致によるエラーを防止するための仕組みとなる。
前回”プログラミングにおける静的型付け/動的型付け言語の違い“で述べたように型システムには、静的型付けと動的型付けの2つのがある。簡単に言うと、静的型付けは、コンパイル時に型検査を実行し、動的型付けは、実行時に型検査を実行するものとなる。
この型システムの導入には、プログラムの品質を向上させ、プログラムの安定性を高めるという利点があり、さらにドキュメントの代わりにコードを読んでコードの理解を容易にするという利点もある。また近年では、自動コード生成や自動最適化などの自動化プロセスへの型システムの適用も行われている。
プログラミング言語には、静的型付けと動的型付けの両方があり、代表的な静的型付け言語は、Java、C++、C#、Scalaで、動的型付け言語は、Python、Ruby、JavaScriptなどがある。
静的型付け言語では、変数の型を事前に宣言する必要があるため、アジャイル的な開発には不向きとなる。これに対して、ウォーターフォール的な大規模開発では静的型付けを行うことで、コードの可読性が向上し、バグの数が減り、また、コンパイル時に型エラーを検出できるため、安全性が高まるという利点が生まれる。
一方、動的型付け言語では、変数の型宣言が必要ないため、コードをより簡潔に書くことができ、開発サイクルが速くなりアジャイル開発には向いているという利点がある。しかし、コンパイル時に型エラーを検出できないため、バグが発生する可能性もあり、大規模な開発時には注意が必要となる。
Clojureと型システム
Clojureは、Rich Hickeyによって開発され、2007年に最初にリリースされましたJava仮想マシン(JVM)上で実行されるLisp系のプログラミング言語となる。
Clojureは、Javaプラットフォームの生産性とスケーラビリティを利用しながら、不変データ構造、高階関数、マクロ、並列性等の関数型プログラミングの哲学を採用している。そのため、大規模なエンタープライズシステムの実装も可能であると同時に、Webアプリケーション、分散システム、データ処理、科学計算、機械学習などの複雑な問題を解決することもできる。
型システムの観点で見ると、Clojureは、静的な型システムと動的な型システムの両方を備えたLisp系のプログラミング言語であるということが言える。これはベースとなるJavaが持つ静的型付け言語という特性と、LISP言語体系が持つ動的な型システムでの、型宣言を行わない柔軟なコーディングに対応しているという特性を兼ね備えていることから、コードの信頼性を高め、同時にコードの柔軟性を維持することができるものとなっている。
型システムという観点から見たClojureには、以下のような特徴がある。
- プリミティブ型のサポート: Clojureの型システムは、Java仮想マシン(JVM)の型システムを拡張している。そのため、ClojureはJavaのプリミティブ型(int、long、float、double、booleanなど)をサポートしている。
- 型推論: Clojureは型推論をサポートしている。つまり、変数の型を宣言する必要がない場合がある。Clojureの型推論は、変数の初期化値を基にして行われる。
- レコード型: Clojureは、Javaのクラスやインタフェースを拡張したレコード型をサポートしている。レコード型は、フィールドの一覧を定義し、これらのフィールドにアクセスするための関数を自動的に生成する。
- プロトコル: Clojureには、プロトコルと呼ばれる、多相性のある抽象化を定義するための機能がある。プロトコルは、型に依存せずに、関数を実装するための方法を提供する。
- 型ヒント: Clojureは、型ヒントをサポートしている。これにより、Clojureの関数に対して、引数や戻り値の型をヒントとして指定することができる。
Clojure Specについて
Clojure specは、Clojureに含まれるライブラリの一つで、関数の引数や戻り値、データ構造などの様々な要素に対して、仕様を定義することができ、更にデータの検証と変換も行うことができるツールで、主な特徴は以下のようになる。
- データの検証と変換のためのツール: Clojure specは、あるデータが指定された仕様に合致するかどうかを検証し、変換するバリデーションツールとなる。Clojure specを使用することで、プログラマーはコード内で明示的にデータの検証と変換を行うことができ、プログラムの信頼性や保守性を向上させることができる。
- データ構造や関数の引数と戻り値に対する仕様の定義: Clojure specは、ベクターの要素の型や範囲、マップのキーと値の型などのデータ構造に対する仕様の定義を行うことができる。また、関数の引数と戻り値の型などに対する仕様の定義もできる。そのような仕様を記述することで、コードの意図をより明確に伝えることができ、コードの理解やメンテナンスの負担を軽減することができる。
- コード内での利用: Clojure specは、Clojureのコード内で簡単に利用することができ、Clojureの関数やマクロの中で、引数や戻り値の型を指定することができる。また、Clojure specのライブラリを使って、特定のデータ構造や関数に対する仕様を定義することもできる。
- テストの自動化: Clojure specを使って仕様を定義し、それに基づいてランダムなデータを生成してテストケースを自動生成することができる。ランダムなデータを使用することで、コードの挙動をさまざまなシナリオで自動でテストすることができる。
- 拡張性: Clojure Specは、仕様の定義方法が柔軟であるため、新しいバリデーションルールやジェネレータを用意に追加することができる。これにより、開発者は自分たちのニーズに合わせてClojure Specをカスタマイズすることができる。
Clojure specは、型アノテーションの機能だけでなく、ジェネレータを使ったランダムなコードテストなど様々な機能を持った開発支援ツールとなる。
Clojure Specの使い方
Clojure Specを使うためには、Clojureの標準ライブラリであるclojure.spec
をインポートする必要がある。
(require '[clojure.spec :as s])
以下に、Clojure Specを使用してデータ構造を指定し、検証する例を示す。
- 仕様の定義
def
マクロを使用して、Clojureのデータ構造に対する仕様を定義する。以下の例では、person
というマップに対する仕様を定義している。
(s/def ::name string?)
(s/def ::age pos-int?)
(s/def ::person (s/keys :req [::name ::age]))
上記の仕様では、::name
と::age
は、文字列と正の整数であることをそれぞれ指定している。そして、::person
は、:req
オプションを使用して、::name
と::age
の両方が必須であることを指定している。
Clojure Specでは、以下の型をサポートしている。
-
any?
:任意の値nil?
:nilnumber?
:数値string?
:文字列map?
:マップvector?
:ベクターset?
:セットcoll-of
:コレクション
- データの検証
valid?
関数を使用して、指定されたデータが指定された仕様に準拠しているかどうかを確認できる。以下の例では、person
というマップが::person
の仕様に準拠しているかどうかを確認している。
(s/valid? ::person {:name "Alice" :age 25})
出力
true
- エラーの説明
explain
関数を使用して、指定されたデータが仕様に準拠していない場合に、その理由を説明することができる。以下の例では、person
というマップが::person
の仕様に準拠していない場合に、エラーの詳細を説明している。
(s/explain ::person {:name "Alice" :age -25})
出力
:clojure.spec/invalid
{:person {:age (not (pos-int? -25))}}
上記の出力から、:person
マップの:age
キーの値が正の整数でないため、仕様に準拠していないことがわかる。
- ランダムデータの生成
specのジェネレータはClojureのプロパティテストライブラリtest.checkに依存しており、gen、exercise、testingを使用したい場合には、test.checkのdev依存を宣言する必要がある。そのために、project.cljファイルに以下を追加する。
:profiles {:dev {:dependencies [[org.clojure/test.check "0.9.0"]]}}
また、導入したtest.checkを利用できるようにrequireを追加設定する必要がある。
(require '[clojure.spec.gen.alpha :as gen])
以上よりgen
関数を使用して、指定されたデータ構造に基づいてランダムなデータを生成できることができる。以下の例では、::person
の仕様に基づいてランダムなデータを生成している。
(gen/generate (s/gen ::person))
出力
#:spec-test.core{:name "L92FFX5ZwJ2h6MJ36q8JJ2l", :age 777010}
#:spec-test.core{:name "96", :age 10803}
また、gen/sampleを使うと複数(デフォルトで10個)のデータをランダム生成できる。
(gen/sample (s/gen ::person))
結果は以下のようになる。
(#:spec-test.core{:name "", :age 1}
#:spec-test.core{:name "", :age 2}
#:spec-test.core{:name "", :age 1}
#:spec-test.core{:name "S2", :age 2}
#:spec-test.core{:name "7MAO", :age 3}
#:spec-test.core{:name "5Ox9o", :age 3}
#:spec-test.core{:name "", :age 4}
#:spec-test.core{:name "OeIrL8p", :age 5}
#:spec-test.core{:name "9f66", :age 42}
#:spec-test.core{:name "R62U", :age 2})
これらのデータを使ってコードが正しく動作しているかをテストすることができる
コメント
[…] Clojureと型システムとSPEC […]