Building Flexible Systems
with Clojure and Datomic Stuart Sierra Cognitect
Building Flexible Systems with Clojure and Datomic Stuart Sierra - - PowerPoint PPT Presentation
Building Flexible Systems with Clojure and Datomic Stuart Sierra Cognitect We dont want to paint ourselves into a corner Clojure Flexible Systems Fact-based Context-free Non-exclusive Observable Fact Based
with Clojure and Datomic Stuart Sierra Cognitect
✦ Fact-based ✦ Context-free ✦ Non-exclusive ✦ Observable
✦ Fact = statement about the world ✦ Cannot be invalidated
public class Person { private List<Person> friends; public void addFriend(Person newFriend);
public class Person { private List<Person> friends; public void addFriend(Person newFriend) { if (friends.length() < 500) friends.add(newFriend); else ... mutable? race condition?
Collection: Set Ordered: Vector Associative: Map Command or stack: List
Structured: Map
{:name "Kelly Q." :friends [{:name "Alice"} {:name "Bob"}]} map vector of maps
(defn add-friend [person new-friend] (if (< (count (:friends person)) 500) (update person :friends conj new-friend) {:error :too-friendly}))
(defn add-friend [person new-friend] define function parameters name list vector symbol
(if (< (count (:friends person)) 500) condition
(if (< (count (:friends person)) 500) (update person :friends conj new-friend) then condition
(update person :friends conj new-friend) {:name "Kelly Q." :friends [{:name "Alice"} {:name "Bob"}]} navigation
conj new-friend) [{:name "Alice"} {:name "Bob"}] conjoin vector [{:name "Alice"} {:name "Bob"} {:name "Claire"}]}
update map {:name "Kelly Q." :friends [{:name "Alice"} {:name "Bob"} {:name "Claire"}]} (update person :friends conj new-friend) {:name "Kelly Q." :friends [{:name "Alice"} {:name "Bob"}]}
universal
{:name "Kelly Q." :friends [{:name "Alice"} {:name "Bob"} {:name "Claire"}]} (update person :friends conj new-friend) {:name "Kelly Q." :friends [{:name "Alice"} {:name "Bob"}]} universal data structures
{:name "Kelly Q." :friends [{:name "Alice"} {:name "Bob"}]} {:name "Kelly Q." :friends [{:name "Alice"} {:name "Bob"} {:name "Claire"}]} (add-friend person {:name "Claire"}) domain rule in terms of universal data and operations
get keys vals assoc assoc-in update update-in dissoc merge merge-with select-keys nth map filter remove reduce replace some sort shuffle reverse take drop union difference intersection select subset? superset? join project index rename concat cycle interleave interpose distinct flatten group-by partition split-at split-with frequencies
(defn add-friend [person new-friend] (if (< (count (:friends person)) 500) (update person :friends conj new-friend) ...)) immutable value no race condition
value function value
value function value function value Time
value function value function value Time identity
value function value function value Time identity stable live
(let [current-user (atom {:name "Kelly Q." :friends [{:name "Alice" :name "Bob"}]})]
current-user
{:name "Kelly Q." :friends [{:name "Alice"} {:name "Bob"}]}
current-user
{:name "Kelly Q." :friends [{:name "Alice"} {:name "Bob"}]}
(let [friends (:friends @current-user)] (html [:p "Your " (count friends) " friends"] [:ul (for [friend friends] [:li (:name friend)])])) dereference
{:name "Kelly Q." :friends [{:name "Alice"} {:name "Bob"}]}
(let [friends (:friends @current-user)] (html [:p "Your " (count friends) " friends"] [:ul (for [friend friends] [:li (:name friend)])])) dereference immutable value
current-user
{:name "Kelly Q." :friends [{:name "Alice"} {:name "Bob"}]}
(swap! current-user add-friend {:name "Claire"})
{:name "Kelly Q." :friends [{:name "Alice"} {:name "Bob"} {:name "Claire"}]}
add-friend
change state pure function identity
storage service connection
storage service connection
database value
database value
connection database value transact database value transact database value Time
Entity Attribute Value
kelly :name "Kelly Q." kelly :friends alice kelly :friends bob
Universal Data
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
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
{:name "Kelly Q." :friends [{:name "Alice"} {:name "Claire"}]}
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
State as-of Last Week
{:name "Kelly Q." :friends [{:name "Alice"} {:name "Bob"}]}
(swap! current-user add-friend ...) (transact connection [[:add-friend ...]]) change state pure function identity
identity database value transact database value transact database value Time stable live
✦ Fact = statement about the world ✦ Cannot be invalidated ➡ Immutable values ➡ Using uniform data structures ➡ Incorporating time
✦ 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
{:customer/id "1234abcd" :customer/name "T.J." :billing.customer/id "abcd" :billing.customer/balance 42.00 :widgetCo.customer/id 7980 :widgetCo/region "AUS"} (merge customer billing-customer widgetco-customer)
(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]])) clojure.spec
(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 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)])]))))
✦ 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
✦ Do not obstruct data evolution ✦ Continue correct operation in the
presence of unexpected input
input function input
extra extra input function input extra extra
(defn gold-status [customer] (if (< 100 (:customer/orders customer)) (assoc customer :loyalty/tier :gold) customer)) then: augment and return else: return unchanged condition
(s/fdef gold-status :args (s/cat :customer (s/keys :req [:customer/id :customer/orders]))) clojure.spec minimal required keys
(gold-status {:customer/id "abcd1234" :customer/balance 4200}) ExceptionInfo Call to #'user/gold-status did not conform to spec: In: [0] val: {:customer/id "abcd1234", :customer/balance 4200} fails at: [:args :customer] predicate: (contains? % :customer/orders)
(gold-status {:customer/id "abcd1234" :customer/orders 123 :customer/balance 4200}) {:customer/id "abcd1234" :customer/orders 123 :customer/balance 4200 :status :gold} extra data passed through
➡ Enforce only minimum input requirements ➡ Ignore and pass through “extra” input ✦ Do not obstruct data evolution ✦ Continue correct operation in the
presence of unexpected input
<?xml version="1.0" encoding="utf-8" ?> <description xmlns="http://www.w3.org/ns/wsdl" targetNamespace= "http://greath.example.com/2004/wsdl/resSvc" xmlns:tns= "http://greath.example.com/2004/wsdl/resSvc" xmlns:ghns = "http://greath.example.com/2004/schemas/resSvc" xmlns:wsoap= "http://www.w3.org/ns/wsdl/soap" xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:wsdlx= "http://www.w3.org/ns/wsdl-extensions"> <types> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://greath.example.com/2004/schemas/ resSvc" xmlns="http://greath.example.com/2004/schemas/resSvc"> <xs:element name="checkAvailability" type="tCheckAvailability"/> <xs:complexType name="tCheckAvailability"> <xs:sequence> <xs:element name="checkInDate" type="xs:date"/> <xs:element name="checkOutDate" type="xs:date"/> <xs:element name="roomType" type="xs:string"/> </xs:sequence> </xs:complexType>
✦ Internal state of the system can be
recreated at any point
✦ Output of the system is suffjcient to
recover its state
value function
current state function new input new state
current state function new input new state
log
state input
[[:db/add user :user/name "Leslie"] [:db/add tx :tx/request-id 1234567890]]
✦ Internal state of the system can be
recreated at any point
✦ Output of the system is suffjcient to
recover its state
➡ Minimize necessary information to
completely describe internal state
➡ Store inputs at boundaries
✦ Fact-based ➡ Immutable values, in domain terms, with time ✦ Context-free ➡ Namespaces; consumer control ✦ Non-exclusive ➡ Enforce minimum, pass-through extra ✦ Observable ➡ Log input, point-in-time state, provenance
✦ “The New Normal” blog series, Mike Nygard ✦ Case Studies: cognitect.com, datomic.com ✦ clojure.org ✦ ClojureTV on YouTube ✦ stuartsierra.com