Skip to content

Instantly share code, notes, and snippets.

@noidi
Created September 19, 2012 20:11
Show Gist options
  • Save noidi/3751951 to your computer and use it in GitHub Desktop.
Save noidi/3751951 to your computer and use it in GitHub Desktop.
Why functional programming matters to enterprise programmers

Why functional programming matters to enterprise programmers

tl;dr: Solving a very specific problem using FP tends to produce a very general solution that's applicable to a wide range of problems.

Let's start with some Java code that we'll simplify using functional programming.

class Department {
  Employee getYoungestEmployee() {
    if (employees.isEmpty()) return null;
    Employee result = employees.get(0);
    for (Employee employee : employees) {
      if (employee.getAge() < result.getAge()) {
        result = employee;
      }
    }
    return result;
  }

  Employee getBestPaidEmployee() {
    if (employees.isEmpty()) return null;
    Employee result = employees.get(0);
    for (Employee employee : employees) {
      if (employee.getSalary() > result.getSalary()) {
        result = employee;
      }
    }
    return result;
  }
}

Here's a port of that code to Clojure. Note that it's very un-idiomatic in order to stay true to the Java version.

(defn get-youngest-employee [department]
  (let [result (atom (first (:employees department)))]
    (doseq [employee (:employees department)]
      (if (< (:age employee) (:age @result))
        (reset! result employee)))
    @result))

(defn get-best-paid-employee [department]
  (let [result (atom (first (:employees department)))]
    (doseq [employee (:employees department)]
      (if (> (:salary employee) (:salary @result))
        (reset! result employee)))
    @result))

Both of these functions walk through a sequence of values and update the result (initialized to the first value in the sequence) with each step. In Clojure this operation is abstracted by a function called reduce. As its argument, in addition to a sequence of values to operate on, reduce needs a function that produces a new result from the old result and the next item in the sequence.

(defn get-youngest-employee [department]
  (reduce (fn [result employee]
            (if (< (:age employee) (:age result))
              employee
              result))
          (:employees department)))

(defn get-best-paid-employee [department]
  (reduce (fn [result employee]
            (if (> (:salary employee) (:salary result))
              employee
              result))
          (:employees department)))

The code can be made a bit cleaner using Clojure's syntactic sugar for anonymous functions. We lose the parameter names, but that's OK because we know that a reduction function's parameters are the old result and the next item.

(defn get-youngest-employee [department]
  (reduce #(if (< (:age %1) (:age %2))
             %1
             %2)
          (:employees department)))

(defn get-best-paid-employee [department]
  (reduce #(if (> (:salary %1) (:salary %2))
             %1
             %2)
          (:employees department)))

If we write the if forms on one line and squint a little, they look a bit like Java's ternary operator (a.getAge() < b.getAge() ? a : b).

(defn get-youngest-employee [department]
  (reduce #(if (< (:age %1) (:age %2)) %1 %2)
          (:employees department)))

(defn get-best-paid-employee [department]
  (reduce #(if (> (:salary %1) (:salary %2)) %1 %2)
          (:employees department)))

Now the functions are as simple as they get, but the duplication of code between them is a source of complexity. We'll tackle that in the usual way by extracting the common parts into a new function, which takes the varying parts as parameters. In this case the parts that vary are the function to get the employee's attribute that we're interested in (:age or :salary) and the function to tell which of the attribute values better matches what we're looking for (< or >).

(defn get-employee [better? attr department]
  (reduce #(if (better? (attr %1) (attr %2)) %1 %2)
          (:employees department)))

(defn get-youngest-employee [department]
  (get-employee < :age department))

(defn get-best-paid-employee [department]
  (get-employee > :salary department))

The extracted function is called get-employee, but once it has obtained the list of employees from the department, it only deals with generic items of a generic sequence. If we shift the responsibility of obtaining the sequence to the callers, it becomes completely generic. Naming generic things is frustratingly hard, so I've opted for a name that's undescriptive but reads nicely in a calling form.

(defn get-with [better? attr items]
  (reduce #(if (better? (attr %1) (attr %2)) %1 %2)
          items))

(defn get-youngest-employee [department]
  (get-with < :age (:employees department)))

(defn get-best-paid-employee [department]
  (get-with > :salary (:employees department)))

The bodies of our two employee-getting functions are almost as readable as their names, so the functions aren't pulling their weight anymore. Whoever needs them can call get-with directly.

(get-with < :age (:employees department))

(get-with > :salary (:employees department))

The users of get-with are not limited to operating on employees of a department. They can just as easily use it find the biggest department in the company (i.e. the department with the greatest count of employees).

(get-with > (comp count :employees) (:departments company))

get-with is not even limited to operations on records of domain data. It can be used to find the number that's closest to zero, or the programming language with the longest name.

(get-with < #(Math/abs %) [-8 5 12 -2 3 6])

(get-with > #(.length %) ["Clojure" "Haskell" "F#" "Objective Caml"])
@noidi
Copy link
Author

noidi commented Sep 24, 2012

Yes, Clojure has the built-in functions min-key and max-key that I could have used (thanks to @amalloy for pointing them out :), but I chose to ignore them as I think that this is a good demonstration of how naturally functional code becomes more generic as it is refactored. If I write a proper blog post on the topic, I'll mention that once you know what generic function you need, you'll probably find that it's already implemented in your language's standard library :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment