It's been a whole month. A whole month of Learning Clojure in Public. On this momentous day, I'm very happy to say that I'm completing the final chapter of Brave Clojure and bringing that whole saga to an end.
Today I wanted to complete Chapter 13, revisit parts of Chapter 11 (core.async) and also work a little bit on the next Team Seneca issue. I'm glad to say that I was able to do all of these things.
Let's take a look at Chapter 13 today - Creating and Extending Abstractions with Multimethods, Protocols, and Records.
- Abstractions - tie together disparate details into a conceptual package that you can hold in working memory
- Clojure - abstraction = collections of operations. data types implement abstractions.
- Implementing an abstraction -> associating an operation name with more than one algorithm. (polymorphism).
A multimethod is a tool for defining polymorphic behavior.
- associate a name with multiple implementations by defining a dispatching function which dispatching values that are used to determine which method to use.
(defmulti full-moon-behavior (fn [were-creature] (:were-type were-creature)))
;; method-name dispatch-value
(defmethod full-moon-behavior :wolf
[were-creature]
(str (:name were-creature) " will howl and murder"))
;; method-name dispatch-value
(defmethod full-moon-behavior :simmons
[were-creature]
(str (:name were-creature) " will encourage people and sweat to the oldies"))
(full-moon-behavior {:were-type :wolf
:name "Rachel from next door"})
;; => "Rachel from next door will howl and murder"
(full-moon-behavior {:name "Andy the baker"
:were-type :simmons})
;; => "Andy the baker will encourage people and sweat to the oldies"
You can always extend the multimethod to handle new dispatch values. Multimethods allow dispatch on multiple arguments and arbitrary values.
(defmulti types (fn [x y] [(class x) (class y)]))
(defmethod types [java.lang.String java.lang.String]
[x y]
"Two Strings!")
(types "String 1" "String 2")
;; => "Two Strings!"
Protocols dispatch methods according to an argument's type. More efficient than multimethods for type dispatch. Multimethod is one polymorphic operation whereas a protocol is a collection of one or more polymorphic operations.
With a protocol, you're defining an abstraction. You're reserving names for behavior, but you haven't defined the behavior yet.
(defprotocol Psychodynamics
"Plumb the inner depthds of your data types" ;; docstring
(thoughts [x] "The data type's innermost thoughts") ;; method signature
(feelings-about [x] [x y] "Feelings about self or other")) ;; method signture
;; method signature - name, argument specification and optional docstring
Extend the string data type to implement the Psychodynamics protocol -
(extend-type java.lang.String
Psychodynamics
(thoughts [x] (str x " thinks, 'Truly the character defines the data type"))
(feelings-about
([x] (str x " is longing for a simpler way of life"))
([x y] (str x " is envious of " y "'s simpler way of life"))))
(thoughts "blorb")
;; => "blorb thinks, 'Truly the character defines the data type"
(feelings-about "schmorb")
;; => "schmorb is longing for a simpler way of life"
(feelings-about "schmorb" 2)
;; => "schmorb is envious of 2's simpler way of life"
If you extend a type to implement a protocol, you have to implement every method in the protocol.
Provide a default implementation by extending java.lang.Object
since every type is a descendant of Object
.
(extend-type java.lang.Object
Psychodynamics
(thoughts [x] "Maybe the Internet is just a vector for toxoplasmosis")
(feelings-about
([x] "meh")
([x y] (str "meh about " y))))
(thoughts 3)
;; => "Maybe the Internet is just a vector for toxoplasmosis"
(feelings-about 3)
;; => "meh"
You can use extend-protocol
to define protocol implementations for multiple types at the same time.
(extend-protocol Psychodynamics
java.lang.String
(thoughts [x] "Truly, the character defines the data type")
(feelings-about
([x] "longing for a simpler way of life")
([x y] (str "envious of " y "'s simpler way of life")))
java.lang.Object
(thoughts [x] "Maybe the Internet is just a vector for toxoplasmosis")
(feelings-about
([x] "meh")
([x y] (str "meh about " y))))
Records are custom, maplike data types.
;; name fields
(defrecord WereWolf [name title])
;; records are actually java classes
;; class instantiation interop call
(WereWolf. "David" "London Tourist")
;; => {:name "David", :title "London Tourist"}
;; -> and map-> are factory functions for records
(->WereWolf "Jacob" "Lead Shirt Discarder")
;; => {:name "Jacob", :title "Lead Shirt Discarder"}
(map->WereWolf {:name "Lucian" :title "CEO of YouTube"})
;; => {:name "Lucian", :title "CEO of YouTube"}
Since records are actually Java Classes underneath, you can create a record using the class interop syntax.
->
and map->
are factory functions for records.
If you want to use a record in a different namespace, you have to use :import
instead of :require
.
You can use assoc
on a record but using dissoc
returns a plain map. Accessing record values is faster than accessing map values.
You can extend a record to implement a protocol.
(defprotocol WereCreature
(full-moon-behavior [x]))
(defrecord WereWolf [name title]
WereCreature
(full-moon-behavior [x]
(str name " will howl and murder")))
(full-moon-behavior (map->WereWolf {:name "PewDie" :title "Chairman of YT"}));; => "PewDie will howl and murder"
Maps vs Records - use records when you find yourself creating maps with the same fields over and over. Records tell you that the set of data has some meaning in your application domain and access is more performant than map access.
- Extend the full-moon-behavior multimethod to add behavior for your own kind of were-creature.
(defmethod full-moon-behavior :paul-bro
[were-creature]
(str "Here lies " (:name were-creature) " Paul."))
(full-moon-behavior {:name "Jake" :were-type :paul-bro})
;; => "Here lies Jake Paul."
- Create a WereSimmons record type, and then extend the WereCreature protocol.
(defrecord WereSimmons [name title]
WereCreature
(full-moon-behavior [x]
(str name " is the " title)))
(full-moon-behavior (map->WereSimmons {:name "Damien" :title "Manager of IKEA"}))
;; => "Damien is the Manager of IKEA"
- Create your own protocol, and then extend it using extend-type and extend-protocol.
(defprotocol WereWolves
"Aware Wolf"
(aware-wolf [x] "An enlightened wolf")
(wear-wolf [x] "Feelings about self or other"))
(extend-protocol WereWolves
java.lang.String
(aware-wolf [x] (str "This Wolf is extremely Aware of " x))
(wear-wolf [x] (str "This Wolf is wearing " x)))
(aware-wolf "TikTok")
;; => "This Wolf is extremely Aware of TikTok"
(wear-wolf "shoes")
;; => "This Wolf is wearing shoes"
We in Team Seneca are working on this issue currently - athensresearch/athens#292.
@alaq and @nthd3gr33 made some progress on implementing the "delete page" functionality. Today we got together virtually to discuss how to delete a page and by extension all the child blocks that are within it.
We looked through the code to see if there were any functions already existing that recursively extracted children and found the deepest-child-block
function. Now it doesn't quite do what we want it to do but it did give us some inspiration.
After deconstructing what the function did, we tried to implement a version of Bread First Traversal that would allow us to traverse through the given block's children and add it to a vector.
I had an aha moment with this problem after reading about the tree-seq
function. This function walks through the given data structure in a Depth First fashion and returns a lazy sequence of the nodes.
The solution ended up being quite concise all thanks to the power of the Clojure Core library -
(defn get-children-recursively
"Get list of children UIDs for given block ID"
[id]
(let [document (->> @(pull dsdb '[:block/order :block/uid {:block/children ...}] id))]
(map :block/uid (tree-seq :block/children :block/children document))))
Chapter 13 is fascinating since it shows you how to implement your own Abstractions. Abstractions are wonderful since it allows you to condense complex topics into a easily grokkable nugget.
We took a look at Multimethods, Protocols and Records in Clojure. Clojure defines abstractions as a collection of operations. And Clojure data types implement abstractions. This eases the mental load for us Clojurists, since we just have to understand the capabilities of particular abstraction to effectively use data structures.
30 Days of Learning in Public has been quite the journey. I've learnt how to use Clojure, ClojureScript, tenets of functional programming and contributing to a large open-source project. But it has also been hard to dedicate time everyday and in writing these daily posts. I do think the positives severely outweigh the negatives in this case.
To wrap up, Let's take a look at what I've done today
- Complete Chapter 13
- Revisit Chapter 11 (not bankruptcy)
- Figure out how to get all children UIDs for a given block ID