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