📝Clojure 型とレコード | Types, Records

📝Clojure 型とレコード | Types, Records

up: 📁Clojure Expression Problem

Clojure レコードとは #

データの抽象を扱うbetterなマップ. (ref.clojuredocs)

defrecord を利用することで,独自の抽象データ型を定義することができ,さらにdefprotocolで定義したメソッドを組み込むことができる.

内部の仕組みではクラスを生成しているのでJavaのクラスのようにOOが持つ特性が利用可能になるというわけだ(cf. defstruct ではクラスを生成しないのでこの特性は利用不可).

ref: 💡考察: データと操作は分けるを深ぼる

Clojure Record Basics #

defrecordで定義したFooというrecordには ->Foo, map->Foo というコンストラクタが自動生成されるためこれをもちいることでRecordを引数付きで生成できる.

  • ->Foo は 引数からrecord生成.
  • map->FooはMapからrecord生成.
(defrecord XYZ [foo bar])

(def xyz (->XYZ "foo" "bar"))

(:foo xyz)

(assoc xyz :boo "boo")

:keywordでRecordのフィールドを読むことができる. .-:keywordは古い.

assocやupdate-inを利用することでRecordを更新することができる. この場合, 新たなRecordが返される(persistent なClojureの設計). ただし, dissocによってフィールドを取り除こうとするとマップが返される.

^Fooのように ^ をつかって型ヒントに使える(ref:Clojure:型ヒント).

record名は大文字から始まることが多い(Foo, Bar). これらは慣例で言語の制約ではないが従うほうがいい.

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

条件に応じてRecordを生成するには? #

これも make-xxx みたいな感じで内部実装を隠蔽してつつパラメータによって生成するレコードを返すようなものはよく見かける.

これはいわゆる Factory Method のように生成するオブジェクトをカプセル化する方法.

この内部でさらにmultimedhodをつかってdispatchしてもかっこいいがそれほど条件分岐が複雑でないならcondでいい. もうすでにmake-xxxで隠蔽しているところで賢いので.

異なる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が泣いたはず.


一部のRecordで同じデータを共有するには? #

初期化処理のなかでなんとかする.

ref. コンストラクタのようにRecordを初期化するには?

一部のRecordで同じprotocol実装を共有するには? #

Recordにメソッドを組み込む場合はProtocolを用いるが, プロトコルには実装が必要であり定義ごとに記述する. 複数のRecordのうち一部だけメソッドや処理内容が共通なので共通化したい場合, つまりdefault implementationのようなものをする場合.

ref. Clojure - mix protocol default implementation with custom implementation - Stack Overflow

差分のある部分のみprotocolで実装して共通部分はわざわざprotocolに組み込むのではなくて単純な関数として定義する.

Clojureの世界は少数の型とそれを操作するたくさんの関数でなりたつため, OOの世界のようなオブジェクトだらけの世界とはちがう.

💡Clojure Records Insignts #

Thinking in Data #

Thinking in Data, Stuart Sierra さんの講演.

おそらく一番わかりやすかった説明動画.

以下のことを強烈に主張.

  • Everything is a Map,全てはMapである.OOPから来た人たちはすぐにクラスみたいなデータ構造を定義しようとするんだ.
    • Record Are Not Schemes,Recordはスキーマではない.
    • なぜClojureのMapをつかわないんだ!
    • 定義するものはよく考えればだいたい不要でしょう.
    • Recordは必要になるまで導入しない.
    • defrecordでtypoしたらどうする?
  • Recordは以下の場合のみにつかう.
    • 単なる性能のための最適化.
    • 汎用的な動作を追加する場合のみ利用する
  • ポリモーフィックでない操作はいらない!

つまりせっかくClojureのMapが素晴らしいのでなぜそれをつかわないんだ?ということ.

シンプルでどんなケースにも対応できる数個のデータ構造は, クソみたいな100個のクラスよりも力がある.

💡Joy of Clojureからのインサイト #

実装レベルでは, mapはPersistentHashMap, つまりシーケンス. 一方RecordはコンストラクタをもつJavaクラスとして定義される.このことにより, メモリ効率ではClassのほうが優れている.

開発のドキュメント作成においてもRecordを作成して特定の要素を属性に持たせたほうがよい?

ref. 🏷The Joy of Clojure

💡プログラミング 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をもちいなくてもいいかも. 判断基準はシンプルなものを選ぶ.

🤔なぜdeftypeとdefrecordの2つがあるのか? #

結論, とりあえず必要になるまではdefrecordをつかいdeftypeは使わない.

deftypeは低レベルのデータ構造向けのもので, deftypeを元にdefrecordが構築されている. deftypeは関数値のみを扱うため,呼び出し時のオーバヘッドを節約できる. しかしほとんどの場合 defrecordがいい.

以下のフローチャートもわかりやすい.


ほとんどのオブジェクト指向言語は明確に以下の2つを分類して設計される.

  • プログラミングのドメインのもの
  • アプリケーションのドメインのもの

ref: なぜdeftypeとdefrecordの両方があるのか?


プログラミングのカスタムなデータ型を定義するのが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で十分. 複雑さを回避するための抽象化は動的言語でさくっと開発するには適さない.

💡defstructは古い(deplicated) #

defrecordは構造体の機能を提供するが, defrecordの登場によってdefstructは不要な方向へ向かっているらしい. ClojureScriptではdefstructは採用されていない.

📚Programming Clojure(2nd)ではしばしばdefstructが登場していたがこれはdefrecordで置き換えたほうが良さそう🤔.