When I first started learning about functional programming, I really wanted an answer to this question. I needed an answer to this question. Before learning about the benefits of each approach, before learning about the unique features in this language or that language, and certainly before learning about monads, I wanted to know: What is the essential difference between imperative and functional programming? If you've gone looking for an answer to this question, then you know it's pretty hard to find.
I think I know the answer now, and I think I found the perfect coding task to help illustrate it. I'm using F# for the code snippets, but don't get hung up on the syntax. The code snippets are best read as pseudo-code.
Suppose we want to render a 10 x 30 map in the console as shown below. The map includes the location of the Enterprise (E) and the location of a Klingon Warbird (K).
****************************** * * * K * * * * * * E * * * * * * * ******************************
Let's establish the dimensions of the map and the locations of our adversaries:
let width, height = (30, 10) let ex, ey = (14, 6) // Enterprise let kx, ky = (24, 3) // Klingon warbird
Now we're ready to render the map. We'll start with the imperative approach.
Imperative programming is about doing, so, to render the map, we will loop through each row and column and draw the appropriate character:
let imperativeRenderMap = for row in 1..height do for col in 1..width do if row = 1 || row = height || col = 1 || col = width then Console.Write("*") else if row = ey && col = ex then Console.Write("E") else if row = ky && col = kx then Console.Write("K") else Console.Write(" ") Console.WriteLine()
Functional programming approaches the task from a different perspective.
Functional programming is about describing, so, to render the map, we will create three expressions. The first expression describes the character to be rendered at a specific point on the map:
let characterAt point = match point with | (row, col) when row = 1 || row = height || col = 1 || col = width -> "*" | (row, col) when row = ey && col = ex -> "E" | (row, col) when row = ky && col = kx -> "K" | _ -> " "
The second expression builds on the previous expression to describe a row on the map:
let rowAt row = String.Concat [for col in 1..width -> characterAt (row, col)]
The third expression builds on the previous expression to describe all of the rows on the map:
let map = [for row in 1..height -> rowAt row]
And then we render the map:
let functionalRenderMap = for row in map do Console.WriteLine(row)
There are some important characteristics of functional programming on display in the above snippet:
- Each expression builds on, or composes, the previous expression. Composing expressions and functions is the bread and butter of functional programming.
- Writing to the console, which is considered a side effect, is postponed until the very end. This allows our three expressions to remain pure: the same inputs always produce the same outputs without any side effects.
Some common questions and observations:
I could refactor the imperative snippet into three functions and have it wait until the very end to write to the console.
Yes, you could, and in doing so you would be refactoring toward functional programming.
I could refactor the functional snippet into a single expression that writes to the console immediately.
Yes, you could, and in doing so you would be refactoring toward imperative programming.
Both of these snippets do the same thing in roughly the same amount of time.
Yes, they do.
So which approach is better?
The one that better matches your mental model. :)
Much of this article was inspired by Costantini and Maggiore's bouncing ball example in Friendly F#.