Clojure gives me the tools to write in a functional style, but I've found myself unconsciously using let
to write imperatively. Why? Probably because it is so easy:
(defn postage-cost [dimensions weight shipping-days]
(let [[height width] dimensions
big? (and (< height 10) (< width 10))
express? (> 2 shipping-days)
multiple (cond
(and express? big?) 5
express? 3
big? 2
:else 1)]
(* multiple weight)))
My imperative solution comes from an imperative problem-solving mindset:
There's nothing especially wrong with such logical step-by-step thinking, but I've written a function that is useless except for a very narrow range of situations. When you tell a computer exactly what to do and exactly how to do it, you end up with a brittle program.
So how should I approach this according to Functional Programming principles? What are those principles? There's no definitive list, but I like these:
First, put your configuration data where it belongs:
(def config
{:pricing
{:express 3
:big 2
:regular 1}
:sizing
{:big-height 10
:big-width 10}
:speed
{:express 2}})
"Garbage in, garbage out"
Every program's job is to transform data (input) into data (output). While thinking imperatively leads me to first seek solutions, thinking functionally leads me to first seek data:
;;; Input
{:width 10
:height 20
:weight 4
:days-to-ship 2}
;;; Some functions do their thing
;;; Output
{:width 10
:height 20
:weight 4
:days-to-ship 2
:express? true
:big? true
:multiplier 5
:price 20}
Now that we have the start and end of our journey through data, we can begin to think of solutions. But these solutions have a sharp focus: transforming data from the bottom-up.
(defn add-price
[{:keys [weight multiplier] :as package}] ;; destructuring
(->> (* weight multiplier)
(assoc package :price))) ;; returns a copy of `package`
(defn add-multiplier
[{:keys [express big regular]}
{:keys [express? big?] :as package}]
(->> (cond
(and express? big?) (+ express big)
express? express
big? big
:else regular)
(assoc package :multiplier)))
(defn add-big
[{:keys [big-height big-width]}
{:keys [height width] :as package}]
(->> (and (< big-height height)
(< big-width width))
(assoc package :big?)))
(defn add-express
[{:keys [express]}
{:keys [days-to-ship] :as package}]
(->> (< days-to-ship express)
(assoc package :express?)))
(defn add-shipping [{:keys [pricing sizing speed] :as config} package]
(->> package
(add-big sizing)
(add-express speed)
(add-multiplier pricing)
add-price))
Our final result couldn't be more clear. Here's where I jump for joy and declare my love for Functional Programming!
"Move fast and break things"
The FP code is five times longer than the imperative solution. That's a huge cost. What kind of code could warrant that kind of effort? I'd propose the following:
It's easy to write imperative code. It happens to me daily. But if I've written anything worth remembering, perhaps it is this simple: don't let
it happen to you too!