-
Notifications
You must be signed in to change notification settings - Fork 4
Generic Mapping with Functors
A functor is a data type whose values may be mapped, or transformed, into other values of the same type and with the same structure. Clojure sequences work as functors and may be transformed by the well-known map
function.
(defn square [x] (* x x))
(map square (range 1 10))
;; (1 4 9 16 25 36 49 64 81)
In this tutorial we'll develop functors that may be mapped in a generic way. Some of the functions and types used below are in the blancas.morph.core
namespace.
(use 'blancas.morph.core)
The great advantage of high-order functions, like map
, is that we can take a function that works on a simple value and use it to transform a more complex type, such as a sequence. We may apply the same technique for our own data types. In the following example, we define a function to map over the cost and price of some part value.
(defrecord Part [name cost price disc])
(def p1 (->Part "tire" 55000 7500 800))
(def p2 (->Part "wheel" 45000 6000 500))
(defcurry map-p
"Applies a price-changing function to a part."
[f p]
(-> p (update-in [:cost] f) (update-in [:price] f)))
(defcurry up
"Price goes up by a percentage."
[pct n] (* n (+ 1 (/ pct 100))))
(map-p (up 8) p1)
;; #user.Part{:name "tire", :cost 59400N, :price 8100N, :disc 800}
(map (map-p (up 8)) [p1 p2])
;; ({:name "tire", :cost 59400N, :price 8100N, :disc 800}
;; {:name "wheel", :cost 48600N, :price 6480N, :disc 500})
The functor above works, and though it may apply any function on cost and price, is not generic enough because we have a map function that works only with Part values. In order to make the type Part
a generic functor we extend it to implement the Functor protocol, which defines a single function fun
. This function dispatches on its first argument this
and also takes the function to be applied. The protocol Functor
is defined as follows:
(defprotocol Functor
(fun [this f]
"Applies a function to a functor's data, producing a new functor."))
For convenience and consistency with map
, Morph core
defines function fmap
, which in turn calls fun
, but whose parameters are reversed: first the function and then the fuctor value. The following example implements the functor Part using the Functor protocol.
(defrecord Part [name cost price disc])
(extend-type Part
Functor
(fun [this f]
(-> this (update-in [:cost] f) (update-in [:price] f))))
(def p1 (->Part "tire" 55000 7500 800))
(def p2 (->Part "wheel" 45000 6000 500))
(defcurry up
"Price goes up by a percentage."
[pct n] (* n (+ 1 (/ pct 100))))
(fmap (up 8) p1)
;; #user.Part{:name "tire", :cost 59400N, :price 8100N, :disc 800}
(map (fmap (up 8)) [p1 p2])
;; ({:name "tire", :cost 59400N, :price 8100N, :disc 800}
;; {:name "wheel", :cost 48600N, :price 6480N, :disc 500})
The above example defines Part
as a functor and declares a function to increase a number by some percentage; then uses fmap
to map a part value to a new one with cost and price up 8%. Last, it uses map
for do a similar mapping for two Part values.
Record Part
may also be defined as a data type like so:
(deftype Part [name cost price disc]
Functor
(fun [this f]
(Part. name (f cost) (f price) disc)))
(defmethod print-method Part [r, ^java.io.Writer w]
(print "name" (.name r) "cost" (.cost r) "price" (.price r) "discount" (.disc r)))
(def p1 (->Part "tire" 55000 7500 800))
(def p2 (->Part "wheel" 45000 6000 500))
(fmap (up 8) p1)
;; name tire cost 59400N price 8100N discount 800
(map (fmap (up 8)) [p1 p2])
;; (name tire cost 59400N price 8100N discount 800
;; name wheel cost 48600N price 6480N discount 500)
fmap
as noted above, is a version of fun
with the arguments in reverse order ([f functor]). This way it works like the familiar map
function. Also, since fmap
is curried, it's easier to use for partial application.
(defrecord Member [id fst lst dues]
Functor (fun [this f] (update-in this [:dues] f)))
(defn double [n] (+ n n))
(def dude (->Member 99 "Joe" "Hacks" 4500))
(def jack (->Member 88 "Jack" "Functor" 1250))
(fmap double dude)
;; #user.Member{:id 99, :fst "Joe", :lst "Hacks", :dues 9000}
(map (fmap double) [dude jack '(30 60)])
;; ({:id 99, :fst "Joe", :lst "Hacks", :dues 9000}
;; {:id 88, :fst "Jack", :lst "Functor", :dues 2500}
;; (60 120))
The following Clojure concrete types implement protocol Functor
when the namespace blancas.morph.core
is loaded:
clojure.lang.PersistentList
clojure.lang.PersistentList$EmptyList
clojure.lang.PersistentVector
clojure.lang.APersistentVector$SubVector
clojure.lang.PersistentHashSet
clojure.lang.PersistentTreeSet
clojure.lang.PersistentArrayMap
clojure.lang.PersistentHashMap
clojure.lang.PersistentTreeMap
clojure.lang.PersistentQueue
clojure.lang.Cons
clojure.lang.IFn
clojure.lang.LazySeq
To illustrate a few of the above:
(fmap double (range 5)) ;; (0 2 4 6 8)
(fmap double [2 4 6 8]) ;; (4 8 12 16)
(fmap double {:foo 50 :bar 80}) ;; ([:foo 100] [:bar 160])
Mapping over a function is composition:
(def squarex2 (fmap double square))
(squarex2 5) ;; 50
<$>
maps a functor to the same supplied value. This function models an assignment on mutable variables inside the functor.
(<$ 37500 dude)
;; #user.Member{:id 99, :fst "Joe", :lst "Hacks", :dues 37500}