Functional Programming Ideas and Patterns
Let's consider the types here:
You can chain things together easily!
(you've probably been doing this for ages!)
For strings:
(str host ":" port "/" resource)
For "fluent" apis:
myService.enableAuth().startService().debug()
For flow control:
if (x == true and y == true) or .....
(+ 1 0) => 1
(concat [1 2] []) => [1 2]
(str "Hello" "") => "Hello"
Consider in general...
(+ some-int nothing) => some-int
(concat some-list empty-list) => some-list
(str some-string empty-string) => some-string
We need to have a concept of nothing!
Or stated more like a math person, 'we need something to give our function, in addition to an "actual" argument, that will always give back the same "actual" argument' (called identity)
It solves a lot of irritating little problems.
Problem: find the max value in a collection.
;; naive imperative approach
;; (but that we totally have all seen in the wild)
(defn find-max-int [col]
(loop [max-value (Integer/MIN_VALUE)
[next-int & remaining] col]
(if (nil? next-int)
max-value
(if (> next-int max-value)
(recur next-int remaining)
(recur max-value remaining)))))
;; this works only because clj already
;; understands that addition has an identity of zero
(reduce + [1])
(= (+ 1 2 3)
(+ 1 (+ 2 3))) => true
(= (concat (concat [1 2] [3 4]) [5 6])
(concat [1 2] (concat [3 4] [5 6])))
(= (max 1 2 3)
(max (max 1 2) 3))
We can group things however we want (associativity)
( = (* 2 (* 3 4))
(* 2 3 4)) ~associative~
(not= (/ 2 (/ 3 4))
(/ 2 3 4)) ~fails~
Consider counting words in a book
- make it smaller, count one page at a time
- make it parallel, you count page 1, I count page two
- make it incremental, I counted pages 1-5 today, tomorrow I can count page 6 and not recount 1-5
This is never listed as a formal rule, but it's still a true thing, monoids operate on two arguments, although it's really impossible to satisfy identity without this being true, but it warrants a glance.
(+ int int) ~closed~ ~associative~ ~identity~ ~2 args~
(inc int) ~closed~ ~not associative~ ~no identity~
Think about Monoids when you're thinking about collections.
Monoids are often a combinatory pattern.
If we have a function fn that accepts 2 arguments of type x
1) fn in closed over x (fn x x) => x (closure)
2) There is some value x1 that (fn x1 _) => _ (identity)
3) We can group this together as we choose
(= (fn x1 (fn x2 x3))
(fn (fn x1 x2) x3)) => true (associativity)
~~~So what! You taught me some math and told me I already know it!
Let's start with reduce, what is reduce? (doc reduce)
clojure.core/reduce
([f coll] [f val coll])
f is a "reducing function", it takes 2 arguments and does ~something~ to them. coll is a collection, items are taken one at a time and supplied to f along with the prior evaluation of f (or an initial starting value val).
(reduce + [1 2 3 4])
(+ (+ (+ 1 2) 3) 4)
(reduce + 0 [1 2 3 4])
(+ (+ (+ (+ 0 1) 2) 3) 4)
At this point our monoid radar is going crazy!
(+ (+ (+ 1 2) 3 ) 4)
We see that these are chaining together safely! (closure)
But maybe reduce actually does this:
(+ (+ 1 2) (+ 3 4))
We don't care (associativity) let the language figure it out.
But wait, there's more. What if
(+ (+ 1 2) (+ 3 4))
Blue is an operation handled by node 0
Red is an operation handled by node 1
Then we aggregate on node 0
More parallelism thanks to associativity.
And when we have
(+ 1 2 3 4 5) then what?
(+ (+ 1 2) (+ 3 4) (+ 5 ..))
Identity!
This is awesome but I don't live in fantasy land of ints, I live in horror land of java interop. I have a collection of:
class CompositeProxyFactoryBeanImpl
public int composedProxyFactoryBeans()
Here Map helps us!
(map #(.composedProxyFactoryBeans %)
collection-CompositeProxyFactoryBeanImpl)
The summary of the pattern:
map the collection to a monoid
reduce the collection
If we want to combine 2 functions, we can do this:
(comp fn1 fn2)
If we want to comp fn1 to fn2 and always get back fn2
we can use identity
(comp identity fn2) => fn2
Can we use comp/identity as a monoid?
Does it work for any functions or just some?
What does it look like in our code when we do this?
What patterns do we notice?