-
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 collections work in a similar way as functors and may be transformed by the well-known map
function, though the result is always a lazy sequence, not the collection's type.
(defn square [x] (* x x))
(map square [2 3 4 5 6 8 10])
;; (4 9 16 25 36 64 100)
This guide covers functors based on custom data types that may be mapped in a generic way. 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 twice [n] (+ n n))
(def dude (->Member 99 "Joe" "Hacks" 4500))
(def jack (->Member 88 "Jack" "Functor" 1250))
(fmap twice dude)
;; #user.Member{:id 99, :fst "Joe", :lst "Hacks", :dues 9000}
(map (fmap twice) [dude jack '(30 60)])
;; ({:id 99, :fst "Joe", :lst "Hacks", :dues 9000}
;; {:id 88, :fst "Jack", :lst "Functor", :dues 2500}
;; (60 120))
Morph extends Clojure's concrete types with support for functors. These new implementations preserve the type of the collection and, except for lists, they're not lazy.
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 twice (range 5)) ;; (0 2 4 6 8)
(fmap twice [2 4 6 8]) ;; [4 8 12 16]
(fmap twice {:foo 50 :bar 80}) ;; {:foo 100, :bar 160}
Mapping over a function is composition:
(defn square [x] (* x x))
(def squarex2 (fmap twice 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}