Welcome to this light introduction to functional programming. Here we will try to explain in few words why functional programming can be applied successfully to reduce the complexity of algorithms and ensure the correctness of the code.
The primary goal of functional programming is expressing complex flows with simple expressions, or, functions. FP heavily borrows from mathematics, as its primary unit is a "function", that can be defined as a building brick that given an input produces an output.
The founding principle on which FP is based are:
- The ability to replace any block of code with the calculated value without altering the program's behaviour (a.k.a.) referential transparency. This implicitly means that a function that returns nothing cannot exist.
- The ability to compose functions one with another to obtain a completely new function that does a composite behaviour
- Using no side effects: every function should be "pure", in a sense that there are no side effects or mutable states. For example there are no setter functions (which do not return values, so they are not purely functional), there are no functions that make IO, and most importantly, there are NO EXCEPTIONS. A function will always return a value, whether the computation goes well, or not.
Any side effect, such as saving to a DB or file system will be postponed until the very end of the program, in order to leave the functions pure.
Or, better, an expression. The definition of expression is "anything that returns a value".
In purely functional languages everything is an expression, so there is no need for a return
statement (even the
if
statement is an expression)
In non purely functional languages, such as Java, we can (since java 8) simulate functional programming using lambdas
and method references, though it's a bit more difficult and verbose.
Referential transparency is all about expressions
is this function referential transparent?
val area = (radius: Int) => Math.PI * math.pow(radius, 2)
and what about this?
var balance = 0
def updateBalance(amount: Int): Int = {
balance += amount
balance
}
updateBalance(150) == updateBalance(150)
In the last snippet we can face unexpected results caused by a hidden global dependency.
An external programmer cannot understand how the computation will result by only reading the function body.
Also think about what can happen if multiple threads call updateBalance
.
A pure function is a function that, given the same input, always returns the same output without producing any side-effect.
--- TODO
In other words, we would like to get closer to the mathematical definition of function, in which we are sure that everything works, thanks to formal models proven over the centuries. What is a "side-effect"? Think about something that mutate external systems like IO operations, access to variables outside local scope or throwing exceptions. It is legal to have local mutations. But real life is full of side-effect, how can we face against this problem? Well, it is not possible to avoid side-effects, but we can model it, describe it through a model (to make "things" referential transparent) and stem them to the borders of our programs. We will face this kind of problem in next sessions. 🤓
Then, we have to compromise and write pure functions where possibile mixing it together with referential transparency.
They give us guarantees: we know that they take some types of parameters and return always a value. We also know that, as we said, they don't touch external variables or fire side-effects. We are sure that composing pure functions we will obtain the expected results! Others benefits are:
- parallelization
- memoization
When we can compose two functions f
and g
(f ∘ g == f(g(x))
)? The domain of f
must be a subset of g
domain.
We want to live a world where every function is a total function. Lets consider this function:
val inverse: (Double) => (Double) = (x) => 1 / x
and try to run inverse(0)
. What do you expect? Not so functional right?
Lets try to refactor it using a functional approach!
val inverse: (Double) => Option[Double] = {
case 0 => None
case x => Some(1 / x)
}
Using Option[T]
we have a function with a total codomain, throwing away exceptions.
And what about reason of failure? You can use Either[L, R]
!
References:
- https://github.com/gcanti/functional-programming
- https://www.youtube.com/watch?v=tKfVI2hGtGQ
- https://medium.com/@olxc/referential-transparency-93352c2dd713
- https://www.sitepoint.com/functional-programming-pure-functions/
Misc: