So first of all, functional programming and imperative programming are
equivalent when it comes down to brass tacks, as shown by the Church-Turing
theorem. Anything that can be done by one can be done by the other. So while I
really prefer functional languages, I can't make a computer do anything that
can't be done in an imperative language.
You'll be able to find all kinds of formal theories about the distinction
with a quick google search so I'll skip that and try to illustrate what I like
using some pseudocode.
So for example, let's say I have an array of integers:
var arrayOfInts = [1, 2, 3, 4, 5, 6]
And I want to turn them into strings:
function turnsArrayOfNumbersIntoStrings(array) {
var arrayOfStrings = []
for (var i = 0; i < arrayOfInts; i++) {
arrayOfStrings[i] = toString(arrayOfInts[i])
}
return arrayOfStrings
}
Later, I'm making a network request:
var result = getRequest("http://some.api")
That gives me a number, and I also want that to be a string:
function getDataFromResultAsString(result) {
var returnValue = {success:, data:}
if (result.success) {
returnValue.success = true
returnValue.data = toString(data)
}
return returnValue
}
In imperative programming, I have to describe how to do what I want.
Those functions are not interchangeable because going through an
array is obviously not the same as doing an if statement. So turning their
values to strings is totally different, even if they both call the same toString
function.
But the shape of those two steps is exactly the same. I mean if you squint a
a little bit, they are the same function.
How they do it has to do with a loop or if statement, but what they do is take
a thing that has stuff in it (either array with ints or request with data) and
turn that stuff into a string, and return.
So maybe we give the things a more descriptive name, that applies to both. They
are both a ThingWithStuff. That is, an array is a ThingWithStuff, and a request
result is a ThingWithStuff. There is a function for each of them, generically
called stuffToString, that can change the stuff inside.
One of the things functional programming has is first order functions: functions
can take functions as arguments. So I could make it more general with something
like this:
function requestStuffTo(modifier, result) {
var returnValue = {success:, data:}
if (result.success) {
returnValue.success = true
returnValue.data = modifier(data)
}
return returnValue
}
function arrayStuffTo(modifier, array) {
var arrayOfStrings = []
for (var i = 0; i < arrayOfInts; i++) {
arrayOfStrings[i] = modifier(arrayOfInts[i])
}
return arrayOfStrings
}
Now the functions for each type keep track of how to
change their internals, but not what. If I want a function that turns an array
or request of ints to strings, I can say what I want:
arrayStuffTo(toString, array)
requestStuffTo(toString, request)
But I don't have to say how I want it, because that was done in the earlier
functions. Later, when I want array and request of say, booleans:
arrayStuffTo(toBoolean, array)
requestStuffTo(toBoolean, request)
Lots of functional languages can tell which version of a function to call by the
type and you can have multiple definitions of the function, one for each type.
So that can be even shorter:
var newArray = stuffTo(toBoolean, array)
var newRequest = stuffTo(toBoolean, request)
I can curry the arguments, then partially apply the function:
function stuffToBoolean = stuffTo(toBoolean)
var newArray = stuffToBoolean(array)
var newRequst = stuffToBoolean(request)
Now they are the same!
Now, when I want to add a new ThingWithStuff type, all I have
to do is implement stuffTo for that thing.
function stuffTo(modifier, maybe) {
if (let Just thing = maybe) {
return Just(modifier(thing))
} else {
return Nothing
}
}
Now I can use the functions I already have with the new thing, for free!
var newMaybe = stuffToBoolean(maybe)
var newMaybe2 = stuffToString(maybe)
Or, I can add a new function:
function stuffTimesTwo(thing) {
return stuffTo((*)2), thing)
}
And I can already use it with any of the things!
var newArray = stuffTimesTwo(array)
var newResult = stuffTimesTwo(result)
var newMaybe = stuffTimesTwo(newMaybe)
I can even take an old function and easily turn it into
one that works on any ThingWithStuff:
function liftToThing(oldFunction, thing) {
return stuffTo(oldFunction, thing)
}
function printThingContents = liftToThing(print)
(ThingWithStuff is usually called Functor, and stuffTo is generally called map)
You can do all the same things in an imperative language, but for example
Haskell already has hundreds of different shape things and thousands of
functions that work on those things. So if I add a new thing, all I have to do
is tell Haskell what shapes it is and I can use those thousands of functions
that already exist. Maybe I want to implement a new kind of Tree, I just say
Tree is a Functor and I can use map to alter its contents. I just say it's an
Applicative and with no more work I can put functions inside it and call it like
a function. I say it's a Semiring and boom, I can add trees together. And all
the other stuff out there that already works for Semirings just works on my
Tree.