Skip to content

Generic Mapping with Functors

blancas edited this page Jan 29, 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 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})

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 works just like fun but it reverses its arguments: first a function and then a 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))
(fmap double dude)
;; #user.Member{:id 99, :fst "Joe", :lst "Hacks", :dues 9000}

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.

(<$ 12500 dude)
;; #user.Member{:id 99, :fst "Joe", :lst "Hacks", :dues 12500}