Building Flexible Systems with Clojure and Datomic Stuart Sierra Cognitect
“We don’t want to paint ourselves into a corner”
Clojure
Flexible Systems ✦ Fact-based ✦ Context-free ✦ Non-exclusive ✦ Observable
Fact Based
Fact Based ✦ Fact = statement about the world ✦ Cannot be invalidated
public class Person { private List<Person> friends; public void addFriend(Person newFriend);
mutable? public class Person { private List<Person> friends; public void addFriend(Person newFriend) { if (friends.length() < 500) friends.add(newFriend); else ... race condition?
Universal Data Collection: Set Structured: Map #{1 7 9 5} {:name "Bob" :age 42} Ordered: Vector ["Alice" "Bob"] Associative: Map {412 "a" Command or stack: List 114 "b"} (println user)
map {:name "Kelly Q." :friends [{:name "Alice"} {:name "Bob"}]} vector of maps
(defn add-friend [person new-friend] (if (< (count (:friends person)) 500) (update person :friends conj new-friend) {:error :too-friendly}))
define name parameters function (defn add-friend [person new-friend] list symbol vector
condition (if (< (count (:friends person)) 500)
condition (if (< (count (:friends person)) 500) (update person :friends conj new-friend) then
{:name "Kelly Q." :friends [{:name "Alice"} {:name "Bob"}]} navigation (update person :friends conj new-friend)
[{:name "Alice"} {:name "Bob"}] conjoin vector conj new-friend) [{:name "Alice"} {:name "Bob"} {:name "Claire"}]}
{:name "Kelly Q." :friends [{:name "Alice"} {:name "Bob"}]} update map (update person :friends conj new-friend) {:name "Kelly Q." :friends [{:name "Alice"} {:name "Bob"} {:name "Claire"}]}
{:name "Kelly Q." :friends [{:name "Alice"} {:name "Bob"}]} universal data structures (update person :friends conj new-friend) {:name "Kelly Q." universal :friends [{:name "Alice"} operations {:name "Bob"} {:name "Claire"}]}
{:name "Kelly Q." :friends [{:name "Alice"} {:name "Bob"}]} (add-friend person {:name "Claire"}) domain rule in terms of universal data and operations {:name "Kelly Q." :friends [{:name "Alice"} {:name "Bob"} {:name "Claire"}]}
(A Few) Universal Operations nth get map concat union keys filter cycle difference vals remove interleave intersection assoc reduce interpose select assoc-in replace distinct subset? update some flatten superset? update-in sort group-by join dissoc shuffle partition project merge reverse split-at index merge-with take split-with rename select-keys drop frequencies
Immutable Values (defn add-friend [person new-friend] (if (< (count (:friends person)) 500) (update person :friends no race conj new-friend) condition ...)) immutable value
value function value
value function value function value Time
identity value function value function value Time
stable live identity value function value function value Time
(let [current-user (atom {:name "Kelly Q." :friends [{:name "Alice" :name "Bob"}]})] current-user {:name "Kelly Q." :friends [{:name "Alice"} {:name "Bob"}]}
dereference (let [friends (:friends @ current-user)] (html [:p "Your " (count friends) " friends"] [:ul (for [friend friends] [:li (:name friend)])])) current-user {:name "Kelly Q." :friends [{:name "Alice"} {:name "Bob"}]}
dereference (let [friends (:friends @ current-user)] (html [:p "Your " (count friends) " friends"] [:ul (for [friend friends] [:li (:name friend)])])) immutable value {:name "Kelly Q." :friends [{:name "Alice"} {:name "Bob"}]}
identity (swap! current-user add-friend {:name "Claire"}) change state pure function current-user {:name "Kelly Q." :friends [{:name "Alice"} {:name "Kelly Q." {:name "Bob"} add-friend :friends [{:name "Alice"} {:name "Claire"}]} {:name "Bob"}]}
connection storage service
(db connection) database connection value storage service
database value (let [db (db connection)] (query db ...) (query db ...) (query db ...)
connection database database database transact transact value value value Time
Universal Data Entity Attribute Value kelly :name "Kelly Q." kelly :friends alice kelly :friends bob
Universal Data in Time Entity Attribute Value Op Tx kelly :name "Kelly Q." add 1000 kelly :friends alice add 1000 kelly :friends bob add 1000
Universal Data in Time Entity Attribute Value Op Tx kelly :name "Kelly Q." add 1000 kelly :friends alice add 1000 kelly :friends bob add 1000 kelly :friends bob retract 1023 kelly :friends claire add 1023
Current State Entity Attribute Value Op Tx kelly :name "Kelly Q." add 1000 kelly :friends alice add 1000 {:name "Kelly Q." kelly :friends bob add 1000 :friends [{:name "Alice"} {:name "Claire"}]} kelly :friends bob retract 1023 kelly :friends claire add 1023
State as-of Last Week Entity Attribute Value Op Tx kelly :name "Kelly Q." add 1000 kelly :friends alice add 1000 kelly :friends bob add 1000 {:name "Kelly Q." kelly :friends bob retract 1023 :friends [{:name "Alice"} {:name "Bob"}]} kelly :friends claire add 1023
(swap! current-user add-friend ...) change state identity pure function (transact connection [[:add-friend ...]])
stable live identity database database database transact transact value value value Time
Fact Based ✦ Fact = statement about the world ✦ Cannot be invalidated ➡ Immutable values ➡ Using uniform data structures ➡ Incorporating time
Context Free
Context Free ✦ Values are complete and self-describing ✦ No out-of-band knowledge required to handle correctly
{:id "1234abcd" :name "T.J."} {:id "abcd" :balance 42.00} {:id 7980 :region "AUS"}
{:customer/id "1234abcd" :customer/name "T.J."} {:billing.customer/id "abcd" :billing.customer/balance 42.00} {:widgetCo.customer/id 7980 :widgetCo/region "AUS"} namespaced keywords
(merge customer billing-customer widgetco-customer) {:customer/id "1234abcd" :customer/name "T.J." :billing.customer/id "abcd" :billing.customer/balance 42.00 :widgetCo.customer/id 7980 :widgetCo/region "AUS"}
clojure.spec (s/def :customer/id (s/and string? #(re-matches #"[0-9a-e]{8}" %))) (s/def :customer/name (s/and string? #(not (str/blank? %)))) (s/def ::Customer (s/keys :req [:customer/id] :opt [:customer/name]]))
(let [user {:customer/name ""}] (s/explain ::Customer user)) val: {:customer/name ""} fails spec: ::Customer predicate: (contains? % :customer/id) In: [:customer/name] val: "" fails spec: :customer/name at: [:customer/name] predicate: (not (blank? %))
(pull db [:customer/name :customer/start-date {:customer/account [:account/id :account/balance]}] customer) {:customer/name "T.J." :customer/start-date #inst"2012-01-24" :customer/account {:account/id 12345 :account/balance 4200}}
(defui UserWidget om.next static om/IQuery (query [this] [:user/name {:user/friends [:user/name]}]) Object (render [this] (let [friends (:user/friends (om/props this))] (html [:p "Your " (count friends) " friends"] [:ul (for [friend friends] [:li (:user/name friend)])]))))
Context Free ✦ Values are complete and self-describing ✦ No out-of-band knowledge required to handle correctly ➡ Use namespaces for globally-unique labels ➡ Let consumers control interactions
Non-exclusive
Non-exclusive ✦ Do not obstruct data evolution ✦ Continue correct operation in the presence of unexpected input
function input input output
function input input output extra extra extra extra
condition (defn gold-status [customer] (if (< 100 (:customer/orders customer)) then: augment (assoc customer and return :loyalty/tier :gold) customer)) else: return unchanged
clojure.spec (s/fdef gold-status :args (s/cat :customer (s/keys :req [:customer/id :customer/orders]))) minimal required keys
Recommend
More recommend