2020-09-30 22:31:51

The functional programming mindset

Hey all! Today I wanted to talk about my experience with functional programming, and in particular about the different ways in which I approach a problem depending on the language that I'm using. This post is mostly for people with little to no experience using a functional language, so if you've ever thought about giving the functional style a go, I hope this will be useful to you.

We're going to be solving a completely made-up and extremely simplified problem with two different languages: JavaScript, in an imperative way, and Haskell, in a functional way. As for the problem, this is the idea: We're given a list of names, and we want to remove all instances of the letter "o" (lowercase) in them. I mean, words without an "o" are just more trustworthy, don't you think? What is with that letter, anyways? Is it a zero with aspirations? Clearly we need to get rid of it right away.

(In all seriousness, I usually prefer to use examples of problems one could encounter in the wild, but I wanted a simple problem that I could break down as much as possible. Plus, you could use a similar method for, say, stripping invalid characters from a list of usernames, so it's not completely useless.)

We won't be concerned with how we get our input, since that's not important right now. We don't care about what to do with the output when we're done, either. We only need to write a function in both cases that takes the input and returns the output.

JavaScript side

Here in JS Land we do things imperatively. That means we think of our programs as a list of instructions to be followed. This is, if nothing else, the most popular way of thinking about a computer program, and for good reasons. It's somewhat closer to what the computer is doing "down at the metal", and arguably the whole point of computers is to automate repetitive tasks and do them much quicker than a human ever could, so it makes sense to think of a task given to a computer as a list of instructions that it must follow one by one. Anything you do on top of it is an abstraction, and those weird, nonsensical, head-in-the-clouds people over at Haskell Land love their abstractions, even if they hide what's really going on.

If you've ever programmed anything before, this should be pretty familiar to you, but I will explain it anyways. In JavaScript, lists are represented by arrays, so our input will be an array containing the data that we want to change:

["Sarah McMonad", "Ethan Semigroupsen", "Lorraine Applicatif"]

Let's start thinking about what we need to do in order to solve our little problem. Well, first of all, we have an array, but working with it directly is going to be difficult. What we actually want is to do something to each of its elements, not to the array itself. This is what for loops were made for:

for (int i = 0; i < names.length; i++) {
  var name = names[i];
  // Whatever we want to do goes here
}

Now, what do we want to do to each element? We want to remove all of its "o"s. Of course, JavaScript has a method that does exactly this, if used with a regular expression:

names[i] = name.replace(/o/g, "");

(If you don't understand regular expressions, don't worry about it, it's not important for this; the g here just means "replace all of the instances, not just the first one you find").

And we're done, right? Putting it all together, our code will run the replace operation on each of the names:

function removeBadLetters(names) {
  for (var i = 0; i < names.length; i++) {
    var name = names[i];
    names[i] = name.replace(/o/g, "");
  }
}

We run this, and voilĂ , we get a set of names that is objectively better than before:

["Sarah McMnad", "Ethan Semigrupsen", "Lrraine Applicatif"]

If you've encountered this a million times before, you may be wondering what the big deal is. Let's hop over to the other side to answer that.

Haskell side

Here in Haskell Land (Glasgow, probably?) we do things functionally. That means we think of our programs as a series of transformations chained together in various ways. For a functional programmer, every program is basically just a function: it takes inputs, it gives outputs, and that's all it does. Similarly, every function is just its own miniature program that takes inputs and gives outputs.

It's important to know that when we talk about functions here in Haskell Land, we're talking about pure functions. This means a function only communicates with the rest of the program it's a part of by means of its inputs and outputs. This is not true over in that dirty, rotten JavaScript Land, where you can declare a variable that is shared by the entire program, and then any function can change it. We don't do things like that here. Remember, each function is a mini-program of its own, and will always give the same output given the same input.

So, let's start thinking about how to accomplish our goal, but this time, functionally. Again, we have a list as an input, and lists in Haskell are represented by... well, a list:

["Sarah McMonad", "Ethan Semigroupsen", "Lorraine Applicatif"]

Again, we have many values that we want to work with. No problem. We simply write a function that passes all the elements of the list through a smaller function. We can do that with map:

removeBadLetters names = map removeOs names

But the function removeOs doesn't exist yet, does it? No worries, we'll declare it now. This time, we'll look at our name as a list of characters (since that's what strings are in Haskell), and our program can simply take only those elements from the list that we want, with filter:

removeOs name = filter (not . isO) name

Again, we don't have a function isO yet, but suppose we did. It would be a mini-program that takes a letter and returns a boolean (True or False). Conveniently enough, we have another mini-program called not that takes a boolean and returns another boolean, its opposite. So we can chain those two together (that's what the . operator does) to form a bigger function, that takes a letter and returns the opposite of whether or not it is an "o". We now pass that to filter, which takes a mini-program as its input, runs it on each of the elements, and keeps only those that return True.

Anyways, the last step is saying what isO actually is:

isO letter = letter == 'o'

Putting it all together:

removeBadLetters names = map removeOs names
removeOs name = filter (not . isO) name
isO letter = letter == 'o'

(Yes, normally we would write type signatures, but I'm skipping that step).

Run our program on the input, and presto, we have a new list of beautiful, "o"-less names:

["Sarah McMnad", "Ethan Semigrupsen", "Lrraine Applicatif"]

Comparing the two

The process of solving the problem was very similar in both cases, but there are a few key differences. When writing functional code, we focus on what our program will do, not how it will do that. We knew that we wanted our program to run the smaller removeOs program on each element, and left the details of that for later. In an imperative language, we have no choice but to explicitly say what instructions the computer will follow, because that's what being imperative means.

Another easy to miss but very important difference is that in JavaScript we were actively altering the state of our program. We have an array loaded into memory, we're running through a series of steps, and at each step we have a very clear concept of what the "current" element is. This is not the case in Haskell. Our function map removeOs names is not an instruction, it's a declaration. Is it stepping through the elements of the list one by one under the hood? Probably, but we don't care. We're telling our program what to do, not how to do it.

Here are some of the reasons why I usually prefer a functional style of programming:

  • Functional programming tends to result in clearer, more readable code. Looking back at both examples above, I think the second one has much less clutter and cuts right to what I want to do with this program, even though we didn't have the built-in replace function.
  • The lack of a shared state means reasoning about the program is much easier. We don't need to mentally keep track of what the list looks like at any step, since there are no steps.
  • Without even needing to, we came up with two extra functions, isO and removeOs that we could use for another purpose in the future. Composability means reusability.

Still, there are just as many reasons why one would choose to program in an imperative style instead:

  • The lack of a shared state means that you have to pass to a function all the information that it needs with its inputs. In more complex programs, this can get very long very quickly. Sometimes we want a shared state.
  • Less abstraction usually means the code is closer to the metal, and thus more efficient. I realize JavaScript wasn't the best example for this, but it is something to consider. Even though Haskell can be a very performant language, all abstractions come at a cost.
  • Some people just find it simpler to think of a list of instructions. I don't, but different mentalities are more natural to different people.

One important note is that you're not locked into a style because of your language of choice. JavaScript is very dynamic and adaptable, and you can also use tools such as map and filter with JavaScript arrays. Plus, there are libraries that let you treat your data as if you couldn't change it, which works really well with a functional style (look up "immutability" if you're interested). On the other hand, in Haskell, there is an entire mechanism built to let you program in an imperative style whenever that's more convenient: monads.

In the end, functional programming is a tool, suited to some tasks better than others. Still, to me it has become an unexpectedly useful tool, and I think it can be worth it for many programmers out there to at least give it a shot.


Posted by Emi Socks | Permanent link