DEV Community

Catherine Galkina for Typeable

Posted on • Originally published at typeable.io

How Does Functional Programming Contribute to Modern Languages?

Author: Vladislav Lagunov

Modern programming languages have a large set of various tools and useful features, which makes it possible to write quite a different code in the same language for the same task.
Programming paradigm is a way of thinking in the first place – the way the programmer thinks about the data representation and processing. In other words, programming paradigm exists in the programmer’s mind and is not a part of the language. Different languages can support a specific paradigm to a varying degree. If we open Wikipedia and start reading about the most popular programming languages, we’ll see that many of them are described as “multi-paradigm”: they allow programming in different styles though some of them will be more convenient to use.

I ♥ FP by https://impurepics.com/
I ♥ FP by https://impurepics.com/

In our recent post, we spoke about the practical applications of Lisp and mentioned, without going into details, that it strongly influenced the development of other programming languages. Now it’s high time to explore this topic in more detail and figure out what contribution functional programming in general (not just Lisp!) has made to the development of other languages. Since we use Haskell as the main programming language, we couldn’t help touching upon this topic.

In this post, we’re going to consider several mechanisms which either emerged in the functional programming languages or were most widely used and popularized by these languages, eventually turning up in originally non-functional languages.

First-class functions

The distinctive feature of FP style, in general, is the wide use of functions which become one of the main development tools. Let’s quickly look through the main definitions describing the differences between the functions and procedures and other similar constructs of non-functional languages.

First-class functions are fancy

Higher-order function is the function that either takes another function as an argument or returns some function as the result. They are also called functionals. This behaviour can be implemented even in pure C using function pointers:

void update_user_balance(int user_id,
                         double (*update_fn)(double))
{
  // ...
  user->balance = update_fn(user->balance);
  // ...
}
Enter fullscreen mode Exit fullscreen mode

First-class functions are the ones which you can manipulate in the same way as any other values: pass as arguments, return as results, assign to variables and structure fields.

Lambda function is the anonymous function. Besides the absence of a name, support of lambda functions removes other language restrictions on function declarations (in some languages, i.e. in C99 standard, function declarations can occur only at the higher level). Support of lambda functions implies that the function can be declared in any location where other expressions are valid. Lambda functions are mostly used in higher-order functions; when used in combination, they provide convenience and significant shortening of the code.

// Example of a lambda function used to print out
// the contents of std::vector
int main() {
  std::vector<int> v;
  v.push_back(1);
  v.push_back(2);
  std::for_each(v.begin(), v.end(), [] (int x) { 
    std::cout << x << '\n'; 
  });
}
Enter fullscreen mode Exit fullscreen mode

Closure is the function that can capture some variables from the context it was declared in, without letting the garbage collector erase the data which can be used in this function for as long as the application has the reference to the function itself. An example in TypeScript:

function createClosures() {
  // The variable can be seen in the functions below
  let counter = 0;
  // The values of fields inc and print are closures
  return {
    inc: () => { counter++; },
    print: () => console.log('counter value: ' + counter),
  };
}

const c = createClosures();
c.inc();
c.inc();
c.print(); // >>> "counter value: 2"
Enter fullscreen mode Exit fullscreen mode

List comprehension

List comprehension allows writing down concisely the processing or generation of lists using existing ones. Miranda was one of the first languages to use such syntax which was adopted by Haskell; later on, similar constructs appeared in “less functional” languages such as Python, C#, Ruby.

Haskell is a leader in adopting new features

As an example, let's consider the code in Python and Haskell that creates word combinations using adjectives and nouns. These two fragments look very much alike and only have minor syntactical differences, don’t they?

# Example in Python
nouns = ["waterfall", "moon", "silence"]
adjectives = ["late", "divine", "blue"]
phrases = [a + " " + n for n in nouns for a in adjectives]
# >>> ['late waterfall', 'divine waterfall',
#   'blue waterfall', 'late moon', 'divine moon',
#   'blue moon', 'late silence', 'divine silence',
#   'blue silence']
Enter fullscreen mode Exit fullscreen mode
-- The same in Haskell
nouns = ["waterfall", "moon", "silence"]
adjectives = ["late", "divine", "blue"]
phrases = [a ++ " " ++ n | n <- nouns, a <- adjectives]
-- >>> ['late waterfall', 'divine waterfall',
--  'blue waterfall', 'late moon', 'divine moon',
--  'blue moon', 'late silence', 'divine silence',
--  'blue silence']
Enter fullscreen mode Exit fullscreen mode

Algebraic data types

These types can also be called ADTs, sum types, discriminatory unions, disjunctive unions, coproducts, and, probably, some other clever words. Maybe you have the idea of such types under different names but, in a few words, this is a compound type containing the discriminant field (which can be called a tag) together with the associated data. The following is the Haskell example of such compound type describing the user’s possible actions in a hypothetical application implementation TodoMVC. Some actions involve the “payload” (a string or an element ID).

data UserAction
  = TextInput String
  | EnterPressed
  | EscPressed
  | CheckTodoItem ItemId Bool
  | EditTodoItem ItemId String
  | DeleteTodoItem ItemId
  | ApplyFilter TodoFilter
Enter fullscreen mode Exit fullscreen mode

Despite its simplicity and usefulness for modelling domain objects, ADTs are rarely supported on a full scale in popular languages and databases. Here are some examples implementing similar types: Enum in Rust, Sealed Classes in Kotlin, std::variant in C++

Pattern Matching

Pattern Matching is a syntactic construct giving access to the data of the structure consisting of one or several alternatives with different sets of fields (the very ADT, algebraic type sum, enum, std::variant, etc. we discussed earlier). Pattern Matching resembles the switch-case operator you all know from imperative languages. However, its main advantage is that the compiler checks the access to alternative fields statically by using the information about the expression type, while the switch-case doesn’t prevent errors related to incorrect access to the fields, missing cases or redundant checks.

Pattern Matching is another technique popularized in functional languages where it turned out to be useful. Now it is adopted and integrated – in various forms – in Python, Java, C#, Rust, and other popular languages.

-- Example of state update function in a hypothetical
-- TodoMVC written in the Elm architecture style.
-- Pattern Matching is used to analyze the
-- user-generated event.
updateTodoList :: UserAction -> TodoState -> TodoState
updateTodoList action oldState = case action of
  TextInput newTitle -> oldState {title = newTitle}
  EnterPressed -> appendNewItem oldState
  EscPressed -> stopEditing oldState
  CheckTodoItem itemId itemChecked ->
    updateItemById itemId (#checked .~ itemChecked)
  EditTodoItem itemId itemTitle ->
    updateItemById itemId (#title .~ itemTitle)
  DeleteTodoItem itemId -> deleteItembyId itemId oldState
  ApplyFilter newFilter -> oldState {filter = newFilter}
Enter fullscreen mode Exit fullscreen mode

Lazy evaluations

In most programming languages evaluation is performed when the value is assigned to the variable; all arguments are evaluated before the function call (strict evaluations). The alternative approach is “lazy” implying that the evaluation is postponed until the value is actually used. Lazy evaluation allows working with infinite data structures, writing declarative code with definitions arranged in the order convenient for reading instead of the order of their evaluation. If you use the DSL approach, lazy evaluations will help you to easily implement such constructs as if-then-else (the values will be evaluated only in the branch you need).

The history of the term can be traced back to lambda calculus, forming part of the theoretical basics of functional programming, so no wonder that it’s used in FP languages. For instance, in Haskell everything is evaluated lazily by default.

Some elements of “laziness” can be found in other languages, even in pure C the operators && and || are lazy as they don’t evaluate their second argument if the first one has been evaluated as 0 or 1, correspondingly. In higher-level languages the more common term is “deferred evaluations” which are implemented using generator functions and the “yield” keyword. Such generators can be found, for example, in Python or Java.

Continuations

Continuation is the “remaining evaluation”, i.e. the description of what is to be done with the result of expression for each subexpression in the program. An expression gets the continuation in the form of an additional argument and when the result is obtained, the current function calls the passed continuation with the evaluated value instead of returning the result directly. Such style of passing the result is called Continuation-Passing Style (CPS).

// Direct style of passing the result 
function getFoo(): Foo {..}

// CPS style
function getFooCPS<A>(cont: (foo: Foo) => A): А {..}
Enter fullscreen mode Exit fullscreen mode

CPS is rarely found immediately in the software source code. Compilers are one of the main areas of its application – as the intermediate format before generating the machine code. Code conversion to CPS allows transforming recursive function calls to tail recursion that can be easily optimized to avoid stack growth during evaluations.

Continuations are a very powerful tool that can be used to implement control structures such as early function exit, explicit call for tail recursion, imperative cycles, etc. For more details on the use of continuations see the example for the Scheme language here.

Futures and promises

Futures, Promises, or Deferred, are a construct containing the evaluation of asynchronous value. They emerged in functional programming as a tool used to simplify parallel evaluations and execute queries in distributed systems.

const getJson = url => fetch(url).then(resp =>
    resp.json());

// Sending 2 serial queries
const getUserInfo = getJson('/current-user-id').then(
  userId => getJson(`/user-info/${userId}`).then(
    userInfo => console.log(
      `Hi ${userInfo.firstName}, your id is ${userId}`)
  )
);
Enter fullscreen mode Exit fullscreen mode

Promises were popularized mainly because of their adaptation and extensive use in the browser. JavaScript execution in the browser is limited by only one execution thread, so blocking while waiting for an HTTP response, as is the case with most platforms, would have led to the page hangup and annoy users. This is why callback functions are used to process responses to HTTP queries in the browser. At the same time, it’s not very convenient to combine such requests and the term “Callback Hell” came up to describe the code that has become illegible due to a large number of callbacks. Promises have allowed partially solving the issue of sending parallel queries and serial query chains:

// Sending 3 parallel queries
const fetchInParralel = Promise.all([
  getJson('/current-user-info'),
  getJson('/shopping-cart'),
  getJson('/recently-viewed-items'),
  ]).then(([userInfo, cart, viewedItems]) => {
    // Display the page using the data sent from the server
    // ...
  })

Enter fullscreen mode Exit fullscreen mode

In many popular languages (such as C#, Java, JavaScript), promises have become the main tool for asynchronous programming.

Monadic interface

The names of many constructs and programming techniques in Haskell were borrowed from category theory and other branches of mathematics. One of such terms – the Monad – has become the object of many memes and jokes about functional programming. There are lots of posts on the Web explaining what “Monads” are in functional languages and how to use them.

Monads have become a local meme in FP world

To put it simply, “monad” is just an interface with two methods that allow combining evaluations into a chain in the same way it’s done in the promise chain example. The promises themselves also illustrate the monadic interface implementation.

// Example of a monad used to generate pseudo-random values;
// parameter А is the type of generated value
class Random<A> {
  // Creation of Random using a random valueе
  static of<A>(value: A): Random<A> {...}
  // Method used to implement the call chain
  chain<A, B>(this: Random<A>, then: (a: A) => Random<B>):
    Random<B> {...}
}

declare function randomNumber(min: number, max: number):
    Random<number>;
declare const randomString: Random<string>;

// Example of using the monad chain
const randomUser: Random<User> = randomString.chain(
  userName => randomNumber(12, 90).chain(
    userAge => Random.of({ name: userName, age: userAge })
  )
);

Enter fullscreen mode Exit fullscreen mode

Among other applications of monads in purely functional languages, such as Haskell, is the side effect encapsulation. As it’s not possible to make a database query, read a file or even print a line in the standard output in these languages by calling ordinary functions, monads are used to perform these actions. At the same time, their application goes beyond side effects – the monadic interface is universal and allows writing generic, laconic, high-level code, which is why the monads are used everywhere in Haskell. Beyond Haskell, monads themselves are not so widely used, but their influence can be seen primarily in programming using promises and in the async-await construct, which we’ll talk about below.

Async

Getting back to the examples of code with promises, you can notice that despite all advantages of the promises, the call chain looks no better than callbacks. The async-await syntactic structure allows taking it a step further and improving the code with the promise chain, turning it into something much like the code with blocking calls.

const getJson = url => fetch(url).then(resp => resp.json());

// Sending 2 serial queries
async function getUserInfo() {
  const userId = await getJson('/current-user-id');
  const userInfo = await getJson(`/user-info/${userId}`);
  console.log(`Hi ${userInfo.firstName}, your id is ${userId}`);
};

Enter fullscreen mode Exit fullscreen mode

The emergence of async-await can be traced back to the researches devoted to functional programming in Haskell and ML which gave rise to the async workflows in F# (2007) and then C# (2011).

Conclusions

Programming languages move on and develop actively, accumulating new, ever more advanced and convenient tools. As you can see, such popular languages as Python or С++ are acquiring a growing number of features, originating from functional programming. More recent languages, such as Scala or Kotlin, support functional tools from the very beginning.

It turns out that functional programming is much closer than might appear at first sight even if you develop in C++ or Java!

We’d be happy to learn about your experience with these or any other functional tricks in your day-to-day development.

Top comments (0)