最近Clojrueを触り始め、簡潔にデータが操作できる面白さに興味を持ちました。
普段、自分はJava、Kotlinをやることが多いので、クラスベースのオブジェクト指向な言語の代替をClojureでやってみます。
実験的な側面のある記事と読んでいただければと思います。
はじめに
Clojureは他の有力なJVM系言語(Java, Kotlinなど)と比較すると関数指向であり、状態の管理をカプセル化する概念のまま実装するのは少しむずかしい。できるけど atom
や ref
などでいろいろ気にしながら状態の変更をコーディングしないといけず、イミュータブルなものとしてコーディングしたほうがいい場面も多い。少なくとも atom
や ref
を使って強引に状態管理しないような実装にしたほうがいい。
イミュータビリティはKotlinでも実装しやすいが、値とふるまいのパッケージング、コンパイルによる縛りあたりはClojureだと難しいので工夫が必要そう。かんたんな値オブジェクトを例に、実装の比較をしてみます。
値オブジェクトを実装する
気に入っているのでKotlinでやっちゃいます。
クラス指向なら
年齢の値オブジェクト。加齢だけが振る舞いとして定義されている。
data class Age (
val value: Int
) {
init {
if (this.value < 0) {
throw RuntimeException("年齢は0才以下になりません")
}
}
fun getOld(): Age {
return Age(this.value + 1)
}
}
Ageクラスを使う。ここまでは普通です。
val now = Age(30)
println(now.getOld())
// ==> 31
Clojureで関数指向に
2022-05-01 更新
clojure.specを使って仕様の定義を導入するといいよ、と教えていただきました。clojure.specについては良質な解説がいくつもあるのでそちらに譲ります。clojure.spec空間はclojure.1.9.0以降に封入されているので事前にclojureバージョンを上げておきます。以下の例においては下記のようにライブラリをrequireしている前提で記載していきます。
(:require [clojure.spec.alpha :as s])
基本的な考え方はクラスベースオブジェクト指向言語でクラスを定義すると同じです。値の性質を定義し、性質を満たす値を操作する関数の仕様および実装を書いていきます。Kotlinの実装に倣い年齢を定義します。
(s/def ::age nat-int?)
自然数、でもいいのですが200才までいきることはさすがに考えづらいので上限を150などにしてもいいと思います。
ついで、歳を重ねます。
(defn get-old [this]
"年を重ねる"
(+ this 1))
上記の関数を、 ::age
を使って仕様を定義しておきます。
(s/fdef get-old
:args (s/cat :this ::age)
:ret ::age)
誤解を生む表現かもしれないですが、クラスベースではない型アノテーションのような機構かなと思います。引数、戻り値それぞれが満たすべき値の性質を述語を使って表現します。この例ではかんたんな述語しか採用してないですが、述語であればなんでもいいので無名関数も記述できます。
関数とその仕様を定義できたのでそれぞれよびだしてみます。
(def next (get-old 30))
; => #'example.value-objects/next
(s/valid? ::age next)
; => true
(println next)
31
; => nil
s/valid?
は値が仕様を満たしていることを確認する述語です。
以下は、投稿時の古い記載です。
データの価値は、関数が決めるということで年齢の値オブジェクトを、レコードと年齢に特化した関数で表現する。
(このへんの背景は最後にある記事をとても参考にしました)。
クラスベースの言語ならコンストラクタにいろいろやらせるが、レコードにはそれはさせない。あくまでデータとラベル付にしておく。
(defrecord Age [value])
代わりに、かんたんなファクトリメソッドを同じ名前空間においておく。動的型付けであるがゆえに、関数と値の紐付けを言語仕様ではしばれない...
(defn create-age [this value]
(if (< value 0) (throw (RuntimeException. "年齢は0才以下になりません")))
(->Age value))
そして、年をとる。この関数は Age を引数に取らない限りおよそ意味をなさないようにしておく。これも同じ名前空間に。
(defn get-old [age]
(let [now (:value age)]
(->Age (+ now 1))))
今。そして歳を取ると...
(def now (create-age 30))
; => Age{:value 30}
(get-old now)
; 評価すると Age{:value 31} になる
型によるモデルのしばりはできないが、名前空間への配置と適切な関数命名および規約の策定ができれば十分モデル駆動な実装もできそうです。