Practical Functional Programming

image

The text of the article is taken from a presentation that I showed on LinkedIn in 2016. The presentation attempted to explain functional programming without using concepts such as “monads,” “immutability,” or “side effects.” Instead, she focuses on how thinking about composition can make you a better programmer, no matter what language you use.

40 years ago, on October 17, 1977, the Turing Prize was awarded to John Bakus for his contribution to the development of high-level programming systems, primarily the Fortran programming language. All Turing Prize winners are given the opportunity to deliver a lecture on their chosen topic during the year in which they receive the prize. As the creator of the Fortran programming language, one would expect Bakus to give a lecture on Fortran’s benefits and future developments in that language. Instead, he gave a lecture entitled “Is it possible to free programming from von Neumann’s style”? in which he criticized some of the major languages ​​of the time, including Fortran, for their flaws. He also proposed an alternative: a functional programming style.

The lecture contrasts traditional programs and their “inability to effectively use powerful combining forms” with functional programs that are “based on the use of combined forms”. In the past few years, functional programming has sparked new interest due to the growth of scalable and parallel computing. But the main advantage of functional programming is whether your program is parallelized or not: functional programming is better in composition.

Composition is the ability to assemble complex behavior by combining simple parts. In computer science lessons, much attention is paid to abstraction: taking a big problem and dividing it into parts. Less emphasis is placed on the opposite: as soon as you realize the small parts, how to put them together. It seems that some functions and systems are easy to put together, while others are much more complicated. But we need to take a step back and ask: what properties of these functions and systems facilitate their layout? What properties make their layout difficult? After reading enough code, the pattern begins to appear, and this pattern is the key to understanding functional programming.

Let’s start by looking at a function that is really well-linked:

String addFooter(String message) {
  return message.concat(" - Sent from LinkedIn");
}

We can easily link it with another function without having to make any changes to our source code:

boolean validMessage(String message) {
  return characterCount(addFooter(message)) <= 140;
}

That's great, we took a small piece of functionality and put it together to do something more. Function users validMessage you don’t even need to be aware of the fact that this function was built from the lesser; it is abstracted as an implementation detail.

Now, let's take a look at a function that is not so well arranged:

String firstWord(String message) {
  String[] words = message.split(' ');
  if (words.length > 0) {
    return words[0];
  } else {
    return null;
  }
}

And then try composing it with another function:

// “Hello world” -> “HelloHello”
duplicate(firstWord(message));

Despite the simplicity at first glance, if we run the above code with an empty message, we get a terrible exception NullPointerException. One option is to modify the dublicate function to handle the fact that its input can sometimes be null:

String duplicateBad(String word) {
  if (word == null) {
    return null;
  } else {
    return word.concat(word);
  }
}

Now we can use this function with the function firstWord from the previous example and just pass a null value null. But this is against composition and abstraction. If you constantly have to go in and modify the components every time you want to do something more, then this is not amenable to layout. Ideally, you want the functions to look like black boxes, where the exact implementation details don't matter.

Zero objects do not compose well.

Let's look at an alternative implementation that uses the Java 8 type Optional (also called Option or Maybe in other languages):

Optional firstWord(String message) {
  String[] words = message.split(' ');
  if (words.length > 0) {
    return Optional.of(words[0]);
  } else {
    return Optional.empty();
  }
}

Now we will try to put it together with the function unchanged. dublicate:

// "Hello World" -> Optional.of("HelloHello")
firstWord(input).map(this::duplicate)

It works! The optional type takes care that firstWord sometimes does not return a value. If a Optional.empty() returns from firstWordthen the function .map just skip function start dublicate. We were able to easily combine functions without the need to modify dublicate. Compare this to null case when we needed to create a function duplicateBad. In other words: null objects are poorly compiled, and options are good.

Functional programmers are obsessed with making things composite. As a result, they created a large set of tools filled with structures that make non-composable code composable. One of these tools is an optional type for working with functions that return valid output only at a specific time. Let's look at some other tools that were created.

Asynchronous code is notoriously difficult to assemble. Asynchronous functions usually receive "callbacks" that are triggered when the asynchronous part of the call completes. For example, the function getData can make an HTTP call to a web service and then run a function on the returned data. But what if you want to make another HTTP call right after that? And then again? Doing this quickly leads you into a situation fondly known as callback hell.

getData(function(a) {  
    getMoreData(a, function(b) {
        getMoreData(b, function(c) { 
            getMoreData(c, function(d) { 
                getMoreData(d, function(e) { 
                    // ...
                });
            });
        });
    });
});

For example, in a larger web application, this results in a very nested spaghetti code. Imagine trying to highlight one of the functions getMoreData in her own method. Or imagine you are trying to add error handling to this nested function. The reason it cannot be linked is because there are many contextual requirements for each block of code: the inner block requires access to results from a, b, c, etc., etc.

Values ​​are easier to put together than functions

Let's look at the toolkit of a functional programmer to find an alternative: Promise (sometimes called Future in other languages). Here is the code now:

getData()
  .then(getMoreData)
  .then(getMoreData)
  .then(getMoreData)
  .catch(errorHandler)

Functions getData now return value Promise instead of accepting a callback function. Values ​​are easier to put together than functions because they do not have the same prerequisites as the callback. Now it’s easy to add error handling to the whole block thanks to the functionality that the object provides us Promise.

Another example of non-composable code, which is less talked about than asynchronous code, is loops or, more generally, functions that return multiple values, such as lists. Let's look at an example:

// ["hello", "world"] -> ["hello!", "world!"]
List addExcitement(List words) {
  List output = new LinkedList<>();
  for (int i = 0; i < words.size(); i++) {
    output.add(words.get(i) + “!”);
  }
  return output;
}

// ["hello", "world"] -> ["hello!!", "world!!"]
List addMoreExcitement(List words) {
  return addExcitement(addExcitement(words));
}

We have compiled a function that adds one exclamation point to a function that adds two characters. This works, but is inefficient because it goes through the loop twice, and not just once. We could go back and change the original function, but, as before, this violates the abstraction.

This is a slightly contrived example, but if you imagine code scattered over a larger code base, it illustrates an important point: on large systems, when you try to break things down into modules, operations on one piece of data will not live together. You must choose between modularity or performance.

With imperative programming, you can only get modularity or just performance. With functional programming, you can have both.

Functional programmer's answer (at least in Java 8) is that Stream. Stream the default is lazy, which means that it goes through the data only when necessary. In other words, a “lazy" function: it starts to do work only when asked about the result (the Haskell functional programming language is built on the concept of laziness). Let's rewrite the above example using instead Stream:

String addExcitement(String word) {
  return word + "!";
}

list.toStream()
  .map(this::addExcitement)
  .map(this::addExcitement)
  .collect(Collectors.toList())

Thus, the loop through the list will pass only once and will call the function addExcitement twice for each item. Again, we need to imagine that our code works with the same piece of data in several parts of the application. Without such a lazy structure as StreamAn attempt to improve performance by combining all the crawls of lists in one place would mean breaking existing functions. With a lazy object, you can achieve both modularity and performance, because bypasses are delayed until the end.

Now that we’ve looked at some examples, let's go back to the task to find out which properties make it easier to create some functions compared to others. We have seen that things like null objects, callbacks, and loops do not compose well. On the other hand, optional types, promises, and streams really stack well. Why is this so?

The answer is that composite examples have a clear distinction between what you want to do and how you actually do it.

All previous examples have one thing in common. A functional way of doing things focuses on what you want the result to be. The iterative mode of action focuses on how you actually get there, on implementation details. It turns out that writing iterative instructions on how to do things doesn't look as good as high-level descriptions of what needs to be done.

For example, in the case of Promise: which in this case makes one HTTP call, followed by another. The question is not relevant and is abstracted: perhaps it uses thread pools, mutex locks, etc., but it does not matter.

Functional programming separates what you want the result to be from how that result is achieved.

That is exactly my practical definition of functional programming. We want to have a clear separation of problems in our programs. The “what you want” part is good and compositional and makes it easy to create large things from the smaller ones. At some point, the “how you do it” part is required, but by separating it, we remove material that is not so composite from material that more composite.

We can see this in real examples:

  • The Apache Spark API for performing calculations on large data sets abstracts the details of what machines it will run on and where the data is stored.
  • React.js describes presentation and makes DOM transformation an efficient algorithm

Even if you do not use a functional programming language, separating what and how from your programs will make them more composable.

image

Learn the details of how to get a sought-after profession from scratch or Level Up in skills and salary by completing SkillFactory paid online courses:


Read more

  • The coolest Data Scientist does not waste time on statistics
  • How to Become a Data Scientist Without Online Courses
  • Sorting cheat sheet for Data Science
  • Data Science for the Humanities: What is Data
  • Steroid Data Scenario: Introducing Decision Intelligence

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *