2

Functional programming insists on telling what to do, rather than how to do.

For example,Scala's collections library has methods like filter, map etc. These methods enable developers to get rid of traditional for loops, and hence so called imperative code.

But what is so special about it?

All I see is a for loop related code encapsulated in various methods in a library. A team working in imperative paradigm could also ask one of its team member to encapsulate all such code in a library and all other team members can then use that library, so we get rid of all those imperative code. Does that mean the team has suddenly transformed from imperative to declarative style?

Mandroid
  • 6,200
  • 12
  • 64
  • 134
  • 2
    But how would such a library look like that were general enough to cover (almost) all loops? The team member would ultimately wind up with folds for each data type. But FP goes beyond that. You can actually generalize across types by using functors. –  Aug 15 '21 at 09:54

2 Answers2

2

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.

Ehren Murdick
  • 209
  • 1
  • 4
1

Let's suppose that you have an algorithm that you want to execute at various places of your source-code. You can implement that again and again, or write a method that does it under the hood and you can call it. Instead of focusing on what's "special" in the latter, in my answer I will focus on differences.

Naturally, if you implement that algorithm over and over again, then it's easy to apply changes at particular places. But the issue is that you might need to apply a specific change at some point to the algorithm. If it's implemented 1000 times in the source-code, then you will need to perform the change 1000 times and then test all the changes to make sure you did not screw up. If the 1000 changes are not exactly the same, then the separate implementations of the same algorithm will start to deviate from each other, making the next such change more difficult, so, over time you will have more and more problems while maintaining those 1000 places.

If you implement a method instead that does the algorithm for you and then you need to change the algorithm, you will have to implement the change exactly once and you can reduce the number of tests, because the 1000 calls to that method will become users of the algorithm, not implementors, so the burden will be focused at a single place, which separates your concern for the algorithm from its uses.

Also, if you have such a method, then you can override it easily.

Example:

Let's suppose that you have a loop to be implemented on a collection. In general, the loop goes through each element and does something.

Now, let's suppose further that you implement something like a deletable collection, that is, each elements inside the collection has an isDeleted field or something of the like. Now, for these collections, you want the loop to skip all deleted elements. If you have 1000 places where you implemented the loop, you will have to look into each of them and see whether the elements could be deletable and if so, apply the skipping logic. It will make your code superfluous, not to mention the mental burden and wasted time while you do the refactoring, because you need to determine where the change needs to be performed. And then, if you made some mistakes, you will have bugs to fix. People not familiar with this code will have a hard time understanding it. While, if you have that loop method being called and that does the loop as it is needed, then the code will be more readable, easier to maintain and less prone to bugs.

Lajos Arpad
  • 64,414
  • 37
  • 100
  • 175
  • Aren't software engineers doing this for long using strategy pattern? And then we have DRY too used traditionally. – Mandroid Aug 15 '21 at 10:10
  • @Mandroid you are correct, this is a best practice and it has been used for a long while now. The key idea is to implement a feature exactly once and reuse it. – Lajos Arpad Aug 15 '21 at 10:43