Skip to content

Generic Mapping with Functors

blancas edited this page Feb 2, 2013 · 8 revisions

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})

Protocol Functor

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:

(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}