Impact of Non Functional Programming
Impact of Non Functional Programming
In order to unveil what defines functional programming let’s start with things it
is definitely not:
With this in mind, what is functional programming? Let’s break it down into its
core principles:
A function is pure if, given the same inputs, it a) always returns the same output
and b) does not have any side effects. At first, it may not be apparent why pure
functions are preferable, but take a look at this example (written in Perl, which
is about the furthest thing from a “functional programming language” that I can
imagine):
https://gist.github.com/m-doughty/1a22ce833f707303d84c4c630044c425
The ‘triple’ function is much easier to unit test, because its result depends
solely on its input. It’s also much easier to run in parallel because it doesn’t
rely on access to shared state. Most importantly, it’s predictable: triple(5) will
always return 15, regardless of how many times we run it or what the state of the
surrounding system is saying.
In the Java world, this is already considered best practice. Alistair Cockburn
explains in his introduction to the Hexagonal Architecture that business logic
should not deal with IO, and these should be kept at the edges of the software and
implemented with interfaces to make them easier to test. Uncle Bob Martin’s Clean
Architecture is, in its essence, a guide to keeping side effects at the edges of
your system designs.
So, the goal of a functional programmer shouldn’t be to eliminate IO, but to move
it to the edges in order to keep our business logic pure, composable, and testable.
From the simplest calculator to the most complex data science model, the purpose of
programming is to take an input or inputs (whether that comes from a user, a
sensor, another application, or anywhere else) and transform it into an output. By
thinking about programming this way — “I have this input and I want this output” —
we can break our code down into functions, plumbed together into pipes, which take
our input data and apply the requisite transformations to turn them into output
data. That plumbing relies on referential transparency.
Immutable State
Once the preserve of niche academic languages, immutable values have now made their
way into most mainstream languages. Whether it’s const in ES6, the final keyword in
Java or val/var in Kotlin, most languages have embraced a future wherein we don’t
modify data structures, but copy them.
This might seem counter-intuitive: why would we want to copy something instead of
changing it? The answer lies in unintended effects:
https://gist.github.com/m-doughty/ee1ec19e7ba313ed886de93d4c4db9cf
This might seem like a contrived example — everyone knows that object assignment
duplicates the reference, right? In complex code, though, it’s actually quite a
common issue. A reference will be duplicated somewhere in the code, then passed
into another function that doesn’t realize it’s working on a shared reference, and
then it’ll update the state of an object being used elsewhere in the system.
Immutable data structures allow us to avoid this possibility entirely.
Haskell actually goes a step further and doesn’t allow values to be reassigned. If
you say x is 3, then x is 3 for the remainder of the lexical scope and there’s no
way to tell the program otherwise.
Recursion
In imperative and object-oriented programming, it’s common to use loops (for,
while, forEach) which have side effects (increasing the iterator, changing the
‘while’ variable, or modifying the structure that’s being iterated over). In
functional programming, we prefer recursion. A recursive function is a function
which calls back to itself with modified input values (otherwise we’d have an
infinite loop, which is predictable but not very useful), until it ‘finishes’
processing.
The most common recursive functions to replace loops (all of which are already
implemented in most languages) are:
map, which takes an input collection and applies a function to each element in the
collection, then returns an output collection of equal size (note: the input
collection is immutable, so it will not be changed in the process);
filter, which takes an input collection and a predicate (a function which returns a
boolean), and tests every input against the predicate, then returns only a
collection of the elements which return true; and
reduce (or fold), which takes an input collection and applies a function which
takes an accumulator and a value and turns it into a single value, eventually
returning a single value (like a String or an Int).
All of these recursive functions are examples of higher-order functions, meaning
that they are functions which take functions as parameters. This is commonplace in
functional programming because it allows us to write highly reusable code and
compose terse data pipelines far more easily.
Functors, which are context wrappers which define how functions can be applied to
their boxed values;
Applications, which are context wrappers which define how boxed functions can be
applied to their boxed values; and
Monads, which are context wrappers which define how a function which returns a
boxed value can be applied to their boxed values.
Maybe, in Haskell, is an example of all three. It defines how functions can be
applied to its value, how boxed functions can be applied to its value, and how
functions which return boxed values can be applied to its value. I really recommend
reading Bhargava’s piece on the subject, as the diagrams make this a lot more
digestible, but if you’ve worked with Options, Futures, Eithers, or anything
similar which wraps a value, chances are you’re already using context day-to-day
(or you’re a banker, in which case, these terms have very different meanings).