Clojure Record/Typeまとめ
- defrecord
- 📝deftype(clojure)
- 📝reify(clojure)
Clojure Records
データの抽象を扱うbetterなマップ. (ref.clojuredocs).
定義(defrecord)
defrecord を利用することで,独自の抽象データ型を定義することができ,さらにdefprotocolで定義したメソッドを組み込むことができる.
内部の仕組みではクラスを生成しているのでJavaのクラスのようにOOが持つ特性が利用可能になるというわけだ(cf. defstruct ではクラスを生成しないのでこの特性は利用不可). ただポイントは, Recordを外側から使う側としてはMapと同じように扱える(クラスだと思わない).
record名は大文字から始まることが多い(Foo, Bar). これらは慣例で言語の制約ではないが従うほうがいい.
(defrecord XYZ [foo bar])
^Fooのように ^ をつかって型ヒントに使える(ref:Clojure:型ヒント).
生成(コンストラクタ)
defrecordで定義したFooというrecordには ->Foo, map->Foo というコンストラクタが自動生成されるためこれをもちいることでRecordを引数付きで生成できる.
- (Foo. )
- ->Foo は 引数からrecord生成.
- map->FooはMapからrecord生成.
(def xyz (->XYZ "foo" "bar"))
(def xyz (map->XYZ {:foo "foo", :bar "bar"}))
フィールドアクセス(getter/setter)
:keywordでRecordのフィールドを読むことができる(keyword accessor). また, クラスなのでドットアクセスもできるがこっちよりもkeywordのアクセスを慣習的によくみる.
(:foo xyz)
(.foo xyz)
assocやupdate-inを利用することでRecordを更新することができる. この場合, 新たなRecordが返される(persistent なClojureの設計). ただし, dissocによってフィールドを取り除こうとするとマップが返される.
(assoc xyz :boo "boo")
- .-:keywordは古い記法.
フィールド更新(assoc/update)
Recordはimutableなデータ型なので, 状態のような使い方をするには, assoc/updateをつかう. すると, 新しいマップができる(古いデータはGCで処理されるのでプログラマーは意識しない).
📝deftype(clojure)
deftypeで宣言するデータ型.
- ClojureはJava Object
- ClojureScriptはJavaScript Object
使いどころはJava/ScriptとのInterop
- https://cljs.github.io/api/cljs.core/deftype
- The power and danger of deftype in clojure and clojurescript | Yehonathan Sharvit
生成
deftype単体では, 固定値の設定のみで複雑なことはできない.
もしコンストラクタ(初期化処理)のようなものをつかうには, 📝reify(clojure)と組み合わせる.
mutableな状態を保持する
- :mutableのmetatagで可変性を指定.
- set!で更新する.
- Recordのようにprotocolを実装できるわけではない.
(deftype Person [^:mutable name ^:mutable age]
Object
(greet [this]
(str "Hello, my name is " name " and I am " age " years old.")))
(def john (Person. "John" 30))
;; 状態を更新する
(set! (.-age john) 31)
🤔なぜdeftypeとdefrecordの2つがあるのか?
結論, とりあえず必要になるまではdefrecordをつかいdeftypeは使わない.
deftypeは低レベルのデータ構造向けのもので, deftypeを元にdefrecordが構築されている. deftypeは関数値のみを扱うため,呼び出し時のオーバヘッドを節約できる. しかしほとんどの場合 defrecordがいい.
deftypeとdefrecordの違いの説明.
ref. なぜdeftypeとdefrecordの両方があるのか?
迷った時のフローチャート
以下のフローチャートもわかりやすい.
現在リンク切れ… GitHubに元ネタあり.
GitHub - cemerick/clojure-type-selection-flowchart
日本語訳がある.
プログラムデータ型とドメインモデル
ほとんどのオブジェクト指向言語は明確に以下の2つを分類して設計される.
- プログラミングのドメインのもの
- アプリケーションのドメインのもの
プログラミングのカスタムなデータ型を定義するのがdeftypeでドメインを定義するのがdefrecord.
Summary: There are two commonly used ways to create new data types in Clojure, deftype and defrecord. They are similar but are intended to be used in two distinct use cases. deftype is for programming constructs and defrecord is for domain constructs.
ref. deftype vs defrecord - Eric Normand
自分の解釈だと, Clojureそのものの機能でライブラリを開発するのではなくてClojureでアプリをつくる私のような開発者はだいたいdefrecordをつかってアプリのドメインを扱うべきだということかな?
そしてガチ開発でなくさくっとClojureを書く程度ならばmapで十分. 複雑さを回避するための抽象化は動的言語でさくっと開発するには適さない.
📝reify(clojure)
📝Clojureプロトコルかインタフェースを実装する無名のデータ型を作り出す.
無名のデータ型はdefでbindされることが前提. 🎨Factory Method的なものと組み合わせる.
ref. https://clojuredocs.org/clojure.core/reify
⚖Record vs reify
recordとreifyの一番の違いは, フィールドアクセス.
RecordはMapのように使えるため, keywordにてMapの属性にアクセスするように値を取得できる.
一方reifyはクロージャを介してのみフィールドにアクセスでき, それは外部からはメソッド呼び出しのように取得できる. 言い換えると何でもかんでも内部にアクセスできない(cf. カプセル化).
Recordの場合も, 結局事前になんらかのロジックで処理をしてからRecordのデータを作成しようとすると, make-hogehogeみたいなインスタンス作成関数を別途用意する必要が有る.
ref. ✅コンストラクタのようにRecordを初期化するには?
一方, reityならば, その使用する場所がmake-hogehogeの中での無名インスタンス生成. 言い換えると, reityをつかうとRecord+コンストラクタのようなことが一度にできる.
⚖factory methodとreify
Factory Methodで見かける気がする. 無名インスタンスを生成する関数の中で使われるが, 関数の先にdefがあるようなパターン.
(def hoge (hoge-generator))
hogehoge-generatorみたいな.
cf. ✅コンストラクタのようにRecordを初期化するには?
⚖extend-type vs reify
既存の型に独自の関数を生やすのはと似ているかもしれないが, reifyは生成時に引数をもらうことができるところ.
(defn generate-hoge [limit]
([limit]
(reify HOGE
(push [this]
... ))))
References
Clojure Records Howto
✅コンストラクタのようにRecordを初期化するには?
RecordはBetter Mapなので初期化ではデータを格納するようにしかIFがなってない. (->HOGEか, せいぜいmap->HOGE).
そこでそこで慣例的によくみかけるのは make-xxx という関数を作成して, 事前にいろいろ計算してletで一時変数としたあとに,最後にRecordを生成してそれを返すhelper 関数を作成する.
;;define Address record
(defrecord Address [city state])
;;define Person record
(defrecord Person [firstname lastname ^Address address])
;;buid the constructor
(defn make-person ([fname lname city state]
(->Person fname lname (->Address city state))))
;;create a person
(def person1 (make-person "John" "Doe" "LA" "CA"))
(defn make-item
[{:keys [id age] :as input}]
{:pre [(string? id)
(number? age)]}
(-> input
(update-in [:id] #(UUID/fromString %))
(update-in [:age] int)
my.model/map->Item))
ref. Clojure: How to Hook into Defrecord Constructor
Clojure: reify はみかたによってはこの Record+初期化(コンストラクタ)を一度にできるような機能といえるかもしれない.
条件に応じてRecordを生成するには?
これも make-xxx みたいな感じで内部実装を隠蔽してつつパラメータによって生成するレコードを返すようなものはよく見かける.
これはいわゆる Factory Method のように生成するオブジェクトをカプセル化する方法.
この内部でさらにmultimedhodをつかってdispatchしてもかっこいいがそれほど条件分岐が複雑でないならcondでいい. もうすでにmake-xxxで隠蔽しているところで賢いので.
💡extend-type vs reifyも参考に. reifyはより動的なオブジェクト生成で利用される.
💡異なるnamespaceで定義したRecordを利用するには?
なんとrequireでエラーをする. その理由は, defrecordとはマクロでありその実態はJavaクラスの生成であるため, これをnamespaceでつかうにはrequireではなくimportが必要らしい(ハマりポイント).
さらにやっかいなのは, ->Fooとかmap->Fooのようなシンタックスはマクロから関数を生成しているようで, importとは別にrequireで取り込む必要がある.
すなわちこういうこと.
(ns hoge.core
(:require
[hoge.foo :refer [->Foo]])
(:import
(hoge.foo Foo)))
わかりにくすぎるな…純粋な全Clojurianが泣いたはず.
追記. これがベストプラクティスかわからないけど, map->Fooや ->Fooを呼び出すラッパー関数を定義してこれを外部から呼びだすようにする.
(defn map-> [m]
(map->Foo m))
こうすると外部ファイルから呼び出すときにimportを記述しなくていいのでよりClojureっぽくなる.
- refs.
- Can’t import clojure records - Stack Overflow
- Using records from a different namespace in Clojure
- pure danger techってブログタイトルが危険そうw
- to. 純粋危険氏
- pure danger techってブログタイトルが危険そうw
一部のRecordで同じデータを共有するには?
初期化処理のなかでなんとかする.
ref. コンストラクタのようにRecordを初期化するには?
一部のRecordで同じprotocol実装を共有するには?
Recordにメソッドを組み込む場合はProtocolを用いるが, プロトコルには実装が必要であり定義ごとに記述する. 複数のRecordのうち一部だけメソッドや処理内容が共通なので共通o化したい場合, つまりdefault implementationのようなものをする場合.
ref. Clojure - mix protocol default implementation with custom implementation - Stack Overflow
差分のある部分のみprotocolで実装して共通部分はわざわざprotocolに組み込むのではなくて単純な関数として定義する.
Clojureの世界は少数の型とそれを操作するたくさんの関数でなりたつため, OOの世界のようなオブジェクトだらけの世界とはちがう.
任意のフィールドをrecordに追加するには?
assocをつかう.
場合によってある属性をメンバに加えたいときは, 定義済みのrecordにassocで追加すればいい. recordは単なるMapの拡張. recordにassocするとrecordが帰ってくる.
注意点はdissocの振る舞い. フィールドがdefrecordで定義済みか否かで挙動が変わる. pre-definedの場合はdissocの戻り値はRecordでなくMapになる.
💡defstructは古い(deplicated)
defrecordは構造体の機能を提供するが, defrecordの登場によってdefstructは不要な方向へ向かっているらしい. ClojureScriptではdefstructは採用されていない.
📚Programming Clojure(2nd)ではしばしばdefstructが登場していたがこれはdefrecordで置き換えたほうが良さそう🤔.
- refs.
Record with Protocol getter UAP
Uniform Access Principleでは呼び元は属性へのアクセスでそれが関数なのかフィールドなのか意識しないことが理想だが, ClojureのRecordだと, フィールドアクセスはkeywordかドットアクセス, 一方protocolで提示したものはドットなしアクセスでバラバラ.
いろいろ工夫はあるものの, あまりよい方法とは思えず, 妥協も必要そう.
Uniform access to Clojure records with probabilistic fields
もしくは, defprotocolではなくdefinterfaceにしてすべてドットアクセスに寄せるか.
💡Clojure Records Insignts
Recordの世界観を理解するにはThinking in Dataという講演を見るがいい.
💡Joy of Clojureからのインサイト
実装レベルでは, mapはPersistentHashMap, つまりシーケンス. 一方RecordはコンストラクタをもつJavaクラスとして定義される.このことにより, メモリ効率ではClassのほうが優れている.
開発のドキュメント作成においてもRecordを作成して特定の要素を属性に持たせたほうがよい?
ref. 🔖The Joy of Clojure
✨Clojure Recordのgetterは必要か?
これはdiscussion的なTopic.
(わたしの意見としては), Recordはそれ自体がMapの拡張であり, 素のシンタックスで十分やりたいことはできるので, getterは不要.
プログラミング Clojureからのインサイト
アプリケーションドメインの情報をクラスを使ってモデル化することには欠点がある. ドメインの知識が, クラス特有のsetterやgetterからなる小さな言語の影に隠れてしまうのだ.
情報を一般化して扱うテクニックは使いづらく, 必要以上にドメインに特化したコードをちまちまと書かざるを得なくなり, 再利用しにくいコードを量産する羽目になる. このためClojureでは, ドメイン特有の情報はなるべくマップを使ってモデリングすることを強く推奨している. コレはデータ型にも当てはまる. そこでレコードの登場だ.
ref. 📚Programming Clojure
🤔なぜ MapではなくRecordなのか?
全体として、レコードは情報を持つあらゆる目的でstructmapよりも優れており、そのようなstructmapはdefrecordに移行するべきだ。プログラミングのための構造にstructmapを使用する可能性は低いだろうが、そのような場合にはdeftypeがはるかに向いている。
ref: Clojure -データ型: データ型とプロトコルには強い主張がある
If you’re making a new domain construct, you don’t want low level. That was a mistake Java made, forcing programmers to write tons of domain classes and deck them out with getters. Each class was a new thing, incompatible with any existing tools. In Clojure, you use defrecord if you want to create a domain type. You can think of records like hashmaps but with their own class.
Java界隈の人の間違いはドメインごとに大量のrecordを定義する. ClojureではHashMapでいい.
Like hashmaps, they have equality and hash semantics defined for you, as you would expect. And they can store arbitrary data using the same access patterns as hashmaps. You can assoc, get, count, etc, on any record. Records will have their own class and can implement protocols and interfaces. So you get the best of using reusable data structures and type-based polymorphism. If you don’t need the polymorphism, you should probably just use a hashmap.
ポリモーフィズムをつかう予定がナケばhashmapでいい.
ref. deftype vs defrecord - Eric Normand
recordはmapの機能を兼ねる. 大は小を兼ねてかつデータ保持としてbetter なのがRecordという論調だな.
ref: clojure.core map
Thinking in Data の動画をみて, 必要になるまではRecordは使わなくていいやと思った.
🤔スコープを制限する目的ならばRecordではなくMapをつかう
defrecordが内部の制御でクラスを生成するのならば, オブジェクト指向の目的の一つである再利用可能な型を定義して, その型に従ってオブジェクトを量産することを前提とするはず.
namespace + defによる定数宣言の代替として, スコープを絞る目的でdefrecordをつかおうとしていた(C言語のenumのような定数の宣言をしようとしていた)が, これは単なる定数としてのMapデータ構造でいいだろう. それらの型を利用してオブジェクトを量産するわけではないのだから.
🤔 名前空間(Namespaces)とRecordは似ている
namespacesというのがデータと操作の対応を環境にbindingsしているものとみたとき, RecordとNamespaceはにているといえないだろうか?
ドメイン駆動設計におけるドメインをそのまま名前空間にしてデータとその操作をnamespaceにbindingsすればわざわざRecordをもちいなくてもいいかも. 判断基準はシンプルなものを選ぶ.