More Capable Functions

Create a multi-arity function:

(defn interest
  ([balance rate] (* balance (/ rate 100)))
  ([balance rate days] (* balance (/ rate 100) (/ days 360))))

(interest 17543.55 1.25) ; 219.294375
(interest 17543.55 1.25 180) ; 109.6471875

It is common for lower-arity functions to call higher-arity functions by filling in arguments with default values to share an implementation:

(defn interest
  ([balance rate] (interest balance rate 360))
  ([balance rate days] (* balance (/ rate 100) (/ days 360))))

(interest 9456.93 0.75) ; 70.926975
(interest 9456.93 0.75 180) ; 35.4634875

Write a variadic function by accepting a variable number of arguments:

(defn pizza-price [basic & extras]
  (+ basic (count extras)))

(pizza-price 17) ; 17
(pizza-price 17 "Salami") ; 18
(pizza-price 17 "Salami" "Extra Cheese" "Olives") ; 20

Write a multimethod that dispatches the function call to its proper implementation depending on the arguments provided:

(defn dispatch [shape]
  (cond
    (and (contains? shape :height) (contains? shape :width)) :rectangle
    (contains? shape :side) :square
    (contains? shape :radius) :circle))

(defmulti area dispatch)

(defmethod area :rectangle [r]
  (* (:height r) (:width r)))

(defmethod area :square [s]
  (* (:side s) (:side s)))

(defmethod area :circle [c]
  (* (Math/pow (:radius c) 2) Math/PI))

(defmethod area :default [x]
  (ex-info "unknown shape" {:shape x}))

(area {:width 3 :height 4}) ; 12
(area {:side 3}) ; 9
(area {:radius 5}) ; 78.53981633974483
(area {:h 7 :a 10 :c 4}) ; Exception

An argument not accounted for in the dispatch function is resolved to :default.

Write a recursive function:

(defn sum [xs]
  (if (empty? xs)
    0
    (+ (first xs) (sum (rest xs)))))

(sum []) ; 0
(sum [1 2 3]) ; 6
(sum [1 2 3 4 5]) ; 15

Write a tail-recursive function:

(defn sum
  ([xs] (sum xs 0))
  ([xs acc]
   (if (empty? xs)
     acc
     (sum (rest xs) (+ (first xs) acc)))))

Enable tail-call optimization by replacing the recursive function call with recur:

(defn sum
  ([xs] (sum xs 0))
  ([xs acc]
   (if (empty? xs)
     acc
     (recur (rest xs) (+ (first xs) acc))))) ; NOTE: recur instead of sum

Refactor tail recursion as a loop:

(defn sum [xs]
  (loop [xs xs
         acc 0]
    (if (empty? xs)
      acc
      (recur (rest xs) (+ (first xs) acc)))))

Document values and function using a docstring:

(def VAT "value added tax rate in percent" 8.1)

(defn hypot
  "Calculates the hypothenuse given the rectangular triangle's legs a and b."
  [a b]
  (Math/sqrt (+ (Math/pow a 2) (Math/pow b 2))))

(hypot 3 4) ; 5.0

Retrieve a docstring programmatically:

(doc VAT) ; value added tax rate in percent

(doc hypot) 
;; Calculates the hypothenuse given the rectangular triangle's legs a and b.

Provide validations of arguments and the return value using pre and post conditions, respectively:

(defn hypot [a b]
  {:pre [(> a 0) (> b 0)]
   :post [(> % a) (> % b)]}
  (Math/sqrt (+ (Math/pow a 2) (Math/pow b 2))))

(hypot 3 4) ; 5.0
(hypot 3 0) ; Assert failed: (> b 0)

The return value is available as % in the post condition.

Exercises

Acceleration and Speed

Write a multi-arity function end-speed to compute the speed given an acceleration a in m/s², a time indication t in s, and—optionally—a starting speed v0 in m/s. Document the function with the formula in a docstring.

Hint: Use the formula v=v0+at. The lower-arity function shall call the higher-arity function.

Test: (end-speed 2 10) shall return 20, and (end-speed 2 10 7.5) shall return 27.5.

Solution
(defn end-speed
  "Calculates the end-speed as v=v0+at."
  ([a t] (end-speed a t 0))
  ([a t v0] (+ v0 (* a t))))

Property Summation

Given the following maps of personal financial data:

(def jack {:income 4500 :balance 12942 :debt 120000})
(def jill {:income 3200 :balance 19172 :debt 250000})
(def jane {:income 4900 :balance 10342 :debt 100000})

Write a recursive function (sum-by [prop & args]) that sums up the given property prop in the provided data. Document the function using a docstring.

Hint: Return 0 if the property isn’t found in the data, or of no arguments are provided. Use apply for the recursive call.

Test: (sum-by :income jack jill) shall return 7700, and (sum-by :debt jack jill jane) shall return 470000.

Solution
(def jack {:income 4500 :balance 12942 :debt 120000})
(def jill {:income 3200 :balance 19172 :debt 250000})
(def jane {:income 4900 :balance 10342 :debt 100000})

(defn sum-by
  "Sums up the given property in args provided."
  [prop & args]
  (cond
    (empty? args) 0
    (not (contains? (first args) prop)) 0
    :else (+ (get (first args) prop) (apply sum-by prop (rest args)))))

Bonus Calculation

Given the following payroll data:

(def dilbert {:salary 120000 :position "Engineer"})
(def wally {:salary 90000 :position "Engineer"})
(def topper {:salary 150000 :position "Sales" :revenue 2750000})
(def ashok {:salary 36000 :position "Intern"})
(def boss {:salary 500000 :position "Manager"})

Write a multimethod bonus that calculates each employee’s bonus based on the following rules:

  1. Engineers earning more than 100000 get a bonus of 10% of their salary. Engineers earning up to 100000 get a bonus of 15% of their salary.
  2. Sales people get a bonus of 10% of their salary and 1% of their revenue.
  3. Interns get a fixed bonus of 2000.
  4. All other employees get a bonus of 0.

Hint: The dispatch function shall differentiate between low- and high-earning engineers. Do not explicitly return :default from the dispatch method for all other employees.

Test: (bonus dilbert) shall return 12000.0, (bonus wally) shall return 13500.0, (bonus topper) shall return 42500.0, and (bonus ashok) shall return 2000.0

Solution
(def dilbert {:salary 120000 :position "Engineer"})
(def wally {:salary 90000 :position "Engineer"})
(def topper {:salary 150000 :position "Sales" :revenue 2750000})
(def ashok {:salary 36000 :position "Intern"})
(def boss {:salary 500000 :position "Manager"})

(defn dispatch [employee]
  (cond
    (= "Engineer" (:position employee))
    (if (> (:salary employee) 100000) :engineer-high :engineer-low)
    (= "Sales" (:position employee)) :sales
    (= "Intern" (:position employee)) :intern))

(defmulti bonus dispatch)

(defmethod bonus :engineer-high [employee]
  (* 0.1 (:salary employee)))

(defmethod bonus :engineer-low [employee]
  (* 0.15 (:salary employee)))

(defmethod bonus :sales [employee]
  (+ (* 0.1 (:salary employee))
     (* 0.01 (:revenue employee))))

(defmethod bonus :intern [employee] 2000.0)

(defmethod bonus :default [_] 0.0)

Fibonacci Numbers

Write a tail-recursive function (fib [n]) that computes the nth Fibonacci number.

Hint: The nth Fibonacci number is defined as the sum of its two predecessors. The first two Fibonacci numbers are 1 and 1. Use loop and recur to implement the function.

Test: (fib 0) and (fib 1) shall return 1, (fib 10) shall return 89, and (fib 45) shall return 1836311903—and finish within within milliseconds (use (time (fib 45)) to check).

Solution
(defn fib [n]
  (loop [a 1
         b 1
         i n]
    (if (= i 0)
      a
      (recur b (+ a b) (- i 1)))))

Blob Eats Blob

Given the following data:

(def red {:weight 80 :strength 50})
(def blue {:weight 90 :strength 40})
(def green {:weight 70 :strength 35})
(def black {:weight 0 :strength 0})

Write a function (merge-blobs [a b]) which simulates a fight between the two blobs a and b. The blob with the higher strength eats up the blog with the lower strength, thereby gaining the defeated blob’s weight and 10% of its strength. Test for the following conditions:

  • Preconditions: Both blobs have a positive weight and strength.
  • Postcondition: The returned weight is higher than the weight of any given blob.

Hint: Use :pre and :post to enforce the conditions.

Test: (merge-blobs red blue) shall return {:weight 170 :strength 54}, and (merge-blobs green black) shall throw an assertion error.

Solution
(def red {:weight 80 :strength 50})
(def blue {:weight 90 :strength 40})
(def green {:weight 70 :strength 35})
(def black {:weight 0 :strength 0})

(defn merge-blobs [a b]
  {:pre [(> (:weight a) 0)
         (> (:weight b) 0)
         (> (:strength a) 0)
         (> (:strength b) 0)]
   :post [(> (:weight %) (:weight a))
          (> (:weight %) (:weight b))]}
  (if (> (:strength a) (:strength b))
    {:weight (+ (:weight a) (:weight b))
     :strength (+ (:strength a) (* 0.1 (:strength b)))}
    {:weight (+ (:weight a) (:weight b))
     :strength (+ (* 0.1 (:strength b)) (:strength a))}))