Polymorphic Codebases
When the Expression Problem is the Problem
Text
;; TextField
{:db/id 19879345345013
:field/name "Name"
:field/type :field.type/text}
;; RadiosField
{:db/id 19879345345014
:field/name "How excited are you?"
:field/type :field.type/radios
:field/list ["A little bit"
"The appropriate amount"
"Very much"]}
Text
(defn field->tx
"Transforms a field into transaction data"
[field]
(cond
;; with subfields
(contains? field :field/fields)
(-> field
(select-keys [:db/id :field/name :field/type])
(assoc :field/fields (mapv field->tx (:field/fields field))))
;; with list
(contains? field :field/list)
(-> field
(select-keys [:db/id :field/name :field/type])
(assoc :field/list (create-list-tx (:field/list field))))
:else
(select-keys field [:db/id :field/name :field/type])))
Expression Problem
The goal is to define a datatype by cases, where one can add new cases to the datatype and new functions over the datatype, without recompiling existing code, and while retaining static type safety.
Philip Walder - 1998
Our Problem
The goal is to add Fields and Features without increasing complexity nor repeating code.
render | store | |
---|---|---|
text | render-input | store-string |
radios | render-radios | store-list |
dropdown | render-dropdown | store-list |
render | store | new-fn | |
---|---|---|---|
text | render-input | store-string | |
radios | render-radios | store-list | |
dropdown | render-dropdown | store-list | |
new-field |
simple dispatch | arbitrary dispatch | |
---|---|---|
closed | case | cond |
extensible | protocols | multimethods |
Polymorphism in Clojure
Text
(defn field->tx
"Transforms a field into transaction data"
[field]
(cond
;; with subfields
(contains? field :field/fields)
(-> field
(select-keys [:db/id :field/name :field/type])
(assoc :field/fields (mapv field->tx (:field/fields field))))
;; with list
(contains? field :field/list)
(-> field
(select-keys [:db/id :field/name :field/type])
(assoc :field/list (create-list-tx (:field/list field))))
:else
(select-keys field [:db/id :field/name :field/type])))
Text
(defmulti field->tx :field/type)
(defmethod field->tx :field.type/text [field]
(select-keys field [:db/id :field/name :field/type]))
(derive :field.type/radios :field.type/list)
(derive :field.type/dropdown :field.type/list)
(defmethod field->tx :field.type/list [field]
(-> field
(select-keys [:db/id :field/name :field/type])
(assoc :field/list (create-list-tx (:field/list field)))))
Text
(defn title-field [field title-id select-ch editing?]
(let [selected? (= title-id (:db/id field))]
(when (= :text (:field/type field))
(dom/i {:class (c/c :icon :icon-column/icon
(when editing? :js/dark)
(if selected? :icon-star :icon-star-empty))
:title "Title field"
:on-click (fn [e]
(.stopPropagation e)
(put! select-ch [(if selected?
:reselect
:title-field)
@field])
nil)}))))
$ grep -Rl ':field/type' src/cljs
src/cljs/view/builder.cljs
src/cljs/view/dashboard.cljs
src/cljs/field/render.cljs
src/cljs/field/search/multi.cljs
src/cljs/field/render/multi.cljs
src/cljs/model/state.cljs
src/cljs/model/record.cljs
Characters
Tension
Resolution
Thanks for Listening
Polymorphic Codebases
By Sebastian Bensusan
Polymorphic Codebases
Talk given at clojuTRE 2015
- 2,058