What is Functional Programming
In the past few years, especially with advancements in processor architecture and availability of multiple cores making the power and perils of concurrency manifesting to ordinary programmers, there has been a significant paradigm shift in programming from the imperative world of Java, C# and C++ to the functional world of Scala, Erlang, Clojure and F#. Gone are the days functional programming being confined to the academic and scientific realms of OCaml and LISP, now more and more programmers are expected to understand not just the syntax of a functional programming language, but also the intricacies and programming model of the paradigm.
There are a number of ways to define the functional paradigm.
The What Vs The How
One simple means, I use to describe the difference in the paradigms is imperative model expresses the mechanics of how a problem is solved, whereas functional model is more concerned with what the program does. From that perspective functional programming is more concise, expressive and idiomatic. The best way to look at this is by using an example. Since Scala is a hybrid language, which supports both paradigms, I have used Scala syntax to demonstrate the difference.
The listing below is how to implement a factorial function imperatively,
scala> def factorial(n: Int) = { | var result = 1 | for(i <- 1 to n) result = result * i | result | } factorial: (n: Int)Int
The listing shows that the program describes how factorial is computed. It loops through the list and build the product cumulatively. To achieve this, it makes use of a mutable variable.
The same can be achieved functionally as shown in the listing below,
scala> def factorial(n: Int): Int = { | if (n == 1) 1 else n * factorial(n - 1) | } factorial: (n: Int)Int
The functional model is lot more concise, and is more about what the program is doing, rather than how it is done. The factorial of a number is the product of that number and the factorial of the number minus one. Instead of using mutable variables and loops, the main tool in the functional arsenal is recursion. One may get concerned about the runtime performance implications of recursion, though most languages will reuse the same call stack, if the function is tail-recursive. That is discussion for another day.
Pure Functions
Other terms that are often used to describe the functional programming paradigm are side effects or rather the lack of them, pure functions and referential transparency. Functional programs are composed or pure functions. What exactly are pure functions? Pure functions are a consistent map of its input parameters to its output parameters, which consistently produce the same result for the same input. They don’t have any side effects, which are,
- They don’t modify mutable state
- They don’t interact with the external world (Doing file I/O, write to the console, draw on the screen etc.)
- They don’t terminate by throwing an exception
Pure functions are also referentially transparent. What is referential transparency? An expression is referentially transparent, if it is replaced by its results through out a program, the outcome will remain the same. A function is pure if the application of it is referentially transparent.
Let us look at the listing below,
scala> val a = "Hello" a: String = Hello scala> val b = a + " World" b: String = Hello World scala> val c = a + " World" c: String = Hello World
The first expression is referentially transparent, because if we replace expressions two and three with the result of the first expression, the program produces the same result. This is because the type String is immutable. Whereas the type StringBuilder is mutable, and hence the first expression in the listing below is not referentially transparent.
scala> val a = new StringBuilder("Hello") a: StringBuilder = Hello scala> val b = a.append(" World") b: StringBuilder = Hello World scala> val c = a.append(" World") c: StringBuilder = Hello World World
What About Pure Functions
So how do pure functions allow us to write better programs?
- Pure functions are only concerned about consistently mapping a known input to a known output. Hence, they are easy to comprehend, because we only need local reasoning, with worrying about the effect of functions on mutable states. This makes them better reusable in wider contexts, there by promoting modularity and composability.
- They can be logically reasoned, hence are less intricate and less error prone
- Lack os shared mutable state allow them to scale better in concurrent models, where state is confined to local call stacks
Leave a Reply