How functional programming helped me recognise patterns and concepts in mathematics
I've never been strong at mathematics, and to this day I don't believe you need to be a skilled mathematician to be a good developer, but as time has gone on I've found more and more value in being able to understand and implement algorithms and expressions, which are fundamentally described in mathematics.
The more natural functional concepts become to me, the more I've become to recognise patterns in expressions - being able to break them down and remove the complications which used to baffle me.
I can now understand why maths and FP fit so naturally, and it's because of the ease of which it is to apply patterns; how easy it is to represent them in code.
This is a strange way to go about picking up mathematics and it wasn't at all intentional. Many problems with functional complexity such as; keeping functions pure but requiring side effects; passing state; error handling etc. Have found elegant solutions fundamentally adapted from abstract mathematics - The monad (from category theory) is a great example.
To help explain what I mean when I say identifying patterns, and how they map onto FP, lets take this statement, which defines the product of logarithm.
Looking at this equation, to solve it would be quite simple; add log(x) to log(y) and that gives us our answer.
log(20 * 30) = log(20) + log(30) = 2.778
This is interesting from a code perspective, it's the same log function (
logb(x)) if we replace
logb() with something we essentially have
F(xy) = F(x) + F(y). The
logb() is irrelevant, we could put any function in there. In fact it doesn't even need to be a function! It's just a type that wraps some value, therefore; we can go a step further and make it generic. Let's look at some code to see how we can work with this.
We will first need to declare a type, which can hold a value.
// directly represents F(x) type F<'a> = F of 'a
We can wrap our values in this new
F type; any value we want, it can even be a function we wrap!
In order to replicate getting the product of our new type and actually be able to do any work, we are going to need to define some functions, which are capable of working on our new elevated values. Reason being, we can't use basic functions such as addition, subtraction etc... But more on that later...
We need to be able to add, subtract, multiple, divide these wrapped values, so in comes pure, apply and fmap to help us build up these functions.
// pure (aka return) - takes a value and wraps it let pure' x = F x // apply - unwraps an elevated function and applies an unwrapped value, // wrapping up the result. let apply (F fn) (F v) = F (fn v) // a common infix for apply, makes it nicer to chain. let (<*>) x fn = apply fn x // takes a regular function and applies it to a wrapped value, // returning a wrapped result. // we can make fmap from both our pure' and apply functions let fmap fn x = pure' fn <*> x // infix for fmap let (<!>) x fn = fmap fn x
These functions are fundamental when working with our wrapped type. They are all well known patterns and are very handy when composing new functions.
fmap is the function we will find most useful here, without going into too much detail you can think of
fmap similar to C#'s IEnumerable<> extension
Select - which unwraps an IEnumerable and applies a normal function to each item in the collection, wrapping up the result. Similar deal with fmap, only our fmap works on the type
F rather than an
Back to the reason why we need these - We have to have a way to compose functions in this higher, elevated world. For example, if we have a wrapped value:
let val = F 10
And we want to add 5, we can't simply do:
let valPlus5 = val + 5 -- compile error
(+) takes 2 number arguments and returns a number
(+) : int -> int -> int. Our
val is not a number; it's an
That's where fmap (or it's infix <!>) comes in!
We need a way to squeeze our F value through an ordinary
(+) 5 function.
let valPlus5 = val <!> ((+) 5) -- F 15
fmap Lets us chain these operations together, just like we can do with
let val = 5 + 10 + 34 + 62 let val' = F 5 <!> ((+) 10) <!> ((+) 34) <!> ((+) 62)
By this point it should be a little clearer how we are going to solve our equation:
F(xy) = F(x) + F(y). But before we are ready, there is one more helper function we can make to get us on our way.
:: lift2 : fn:('a -> 'b -> 'c) -> F<'a> -> F<'b> -> F<'c> let lift2 fn (F v1) (F v2) = F (fn v1 v2)
lift2 is essentially fmap2. It applies a regular function which takes 2 arguments to 2 unwrapped values, it's going to help when we come to solving quotent and power too.
Firstly though, let's go back to that addition stuff we just did and see if lift2 can help tidy it up.
let (++) = lift2 (+) let val' = F 5 ++ F 10 ++ F 34 ++ F 62
lift2 we can compose a function which takes 2 elevated values and applies an ordinary function to it; finally wrapping up the result, which enables chaining.
Now we have the functions defined to work with our type, we can do what we came here to do, find the product of log. Using the same idea we just used for addition, we can also apply to product:
// gets the product of two elevated values // replicates combine - F(xy) = F(x) + F(y) // add is associative (order doesn't matter) let product = lift2 (*) let (>*) = product let inline log10 v = v |> (float >> System.Math.Log10) let productResult = F 20 >* F 30 <!> log10 -- 2.77815125
And there's our result. You will notice that it's only at the last stage of composure do we actually apply
log10 in this case for ease).
Quotent, power and root
We can implement and use the other three laws of logarithms in the same way.
// opposite of product let quotent = lift2 (/) let (>/) = quotent // to power let power = lift2 <| fun x y -> System.Math.Pow(float x, float y) let (>^) = power // square root // we don't need lift2, Sqrt takes only a single argument. let root = fmap System.Math.Sqrt let quotentResult = F 10 >/ F 2 -- F 5 let powerResult = F 10 >^ F 2 -- F 100.0 let rootResult = F 10.0 |> root -- F 3.16227766
We don't really need to write all of this for these simple calculations, it's a big abstraction for a little problem, we aren't gaining much just calculating the product of log. In real terms though there is a lot of benefit from working with monadic types like this, and to recognise these patterns and implement them has helped join a few dots for me.
In practical terms, analysing data; running predictive algorithms (and much more) are all described in the language of maths. Being able to translate these algorithms into practical applications is a good thing, and it starts with recognising patterns and being able to apply them. I'm not really interested in being able understand complicated theory if I can't apply it.
In terms of why we would want to work on an abstraction (like our
F type) in a real program, there are a few reasons; which I will very briefly mention:
- Error handling
- Side effects
For example, we can add a side effect to our
apply function, which logs to the output stream the result of the function. Useful for debugging or logging maybe?
let apply (F fn) (F v) = let fnResult = fn v printfn "applied value: %A, result is: %A" v fnResult F fnResult
Tackling these concepts from a computer science first view has helped me identify similar patterns where they exist in mathematics. It has allowed me to find a connection between FP and the more abstract concepts in maths.
I've dived into a lot of FP here and it's not really the important part, but it's a good example of how we can work with maths in FP in a flexible and usable way.