Forem

Cover image for Functional Programming Explained: A Deep Dive
Leapcell
Leapcell

Posted on

Functional Programming Explained: A Deep Dive

Image description

Leapcell: The Next - Gen Serverless Platform for Web Hosting

Detailed Explanation of Functional Programming

You may have heard of functional programming and even used it for some time. But can you clearly explain what it is?

Searching online, you can easily find many answers:

  • It is a programming paradigm parallel to object - oriented programming and procedural programming.
  • Its most significant feature is that functions are first - class citizens.
  • It emphasizes decomposing the computational process into reusable functions. A typical example is the MapReduce algorithm composed of the map method and the reduce method.
  • Only pure functions without side effects are qualified functions.

All the above statements are correct, but they are not enough. They don't answer the deeper question: why do it this way? This is what this article aims to answer. I will help you understand functional programming and learn its basic syntax in the simplest language.

I. Category Theory

The origin of functional programming is a branch of mathematics called category theory. The key to understanding functional programming is to understand category theory. It is a complex mathematics that believes that all conceptual systems in the world can be abstracted into "categories".

1.1 The Concept of Category

What is a category? A one - sentence definition from Wikipedia is as follows: "In mathematics, a category is an algebraic structure that comprises 'objects' that are linked by 'arrows'."

That is to say, concepts, things, objects, etc., that have certain relationships with each other all form a "category". As long as you can find the relationships between them, you can define a "category" for anything.

For example, various points and the arrows between them form a category. The arrows represent the relationships between the members of the category, and the formal name is "morphism". Category theory believes that all members of the same category are "transformations" in different states. Through "morphism", one member can be transformed into another.

1.2 Mathematical Model

Since a "category" is all objects that satisfy a certain transformation relationship, its mathematical model can be summarized as follows:

  • All members form a set.
  • The transformation relationship is a function.

That is to say, category theory is a higher - level abstraction of set theory. A simple understanding is "set + function". In theory, through functions, all other members of the category can be calculated from one member.

1.3 Category and Container

We can imagine a "category" as a container that contains two things:

  • Value.
  • The transformation relationship of the value, that is, the function.

The following is the code to define a simple category:

class Category {
  constructor(val) { 
    this.val = val; 
  }

  addOne(x) {
    return x + 1;
  }
}
Enter fullscreen mode Exit fullscreen mode

In the above code, Category is a class and also a container, containing a value (this.val) and a transformation relationship (addOne). You may have noticed that the category here is all numbers that differ from each other by 1.

Note that in the following parts of this article, wherever "container" is mentioned, it refers to "category".

1.4 The Relationship between Category Theory and Functional Programming

Category theory uses functions to express the relationships between categories. With the development of category theory, a set of function operation methods has been developed. This set of methods was initially only used for mathematical operations. Later, someone implemented it on a computer, and it became today's "functional programming".

Essentially, functional programming is just the operation method of category theory. It is the same kind of thing as mathematical logic, calculus, and determinants. They are all mathematical methods. It just happens that it can be used to write programs.

So, do you understand why functional programming requires functions to be pure and without side effects? Because it is a mathematical operation, and its original purpose is to evaluate values without doing other things. Otherwise, it cannot satisfy the function operation rules.

In short, in functional programming, a function is like a pipe. A value goes in at one end, and a new value comes out at the other end, with no other effects.

II. Function Composition and Currying

Functional programming has two most basic operations: composition and currying.

2.1 Function Composition

If a value needs to go through multiple functions to become another value, all the intermediate steps can be combined into one function, which is called "function composition".

Image description

For example, if the transformation relationship between X and Y is function f, and the transformation relationship between Y and Z is function g, then the relationship between X and Z is the composed function g ∘ f of g and f.

The following is the code implementation (using JavaScript language). Note that all example codes in this article are simplified. The simple code for composing two functions is as follows:

const compose = function (f, g) {
  return function (x) {
    return f(g(x));
  };
}
Enter fullscreen mode Exit fullscreen mode

Function composition must also satisfy the associative law:

Image description

Image description

compose(f, compose(g, h))
// is equivalent to
compose(compose(f, g), h)
// is equivalent to
compose(f, g, h)
Enter fullscreen mode Exit fullscreen mode

Composition is also a reason why functions must be pure. Because how can an impure function be composed with other functions? How can we ensure that after various compositions, it will achieve the expected behavior?

As mentioned before, a function is like a pipe for data. Then, function composition is to connect these pipes so that data can pass through multiple pipes in one go.

2.2 Currying

The composition of f(x) and g(x) into f(g(x)) has a hidden premise that both f and g can only accept one parameter. If they can accept multiple parameters, such as f(x, y) and g(a, b, c), function composition will be very troublesome.

This is where currying comes in. So - called "currying" is to convert a multi - parameter function into a single - parameter function.

// Before currying
function add(x, y) {
  return x + y;
}

add(1, 2) // 3

// After currying
function addX(y) {
  return function (x) {
    return x + y;
  };
}

addX(2)(1) // 3
Enter fullscreen mode Exit fullscreen mode

With currying, we can ensure that all functions accept only one parameter. Unless otherwise specified in the following content, it is assumed that functions have only one parameter, which is the value to be processed.

III. Functor

Functions can not only be used for the transformation of values within the same category but also for transforming one category into another. This involves the functor.

3.1 The Concept of Functor

The functor is the most important data type in functional programming and is also the basic unit of operation and functionality.

It is first of all a category, that is, a container that contains values and transformation relationships. What is special is that its transformation relationship can be applied to each value in turn, transforming the current container into another container.

Image description

For example, the left - hand circle is a functor representing the category of people's names. When the external function f is passed in, it will be transformed into the category of breakfast on the right.

Image description

More generally, the function f completes the transformation of values (a to b). Passing it into the functor can achieve the transformation of categories (Fa to Fb).

3.2 The Code Implementation of Functor

Any data structure with a map method can be regarded as an implementation of the functor.

class Functor {
  constructor(val) { 
    this.val = val; 
  }

  map(f) {
    return new Functor(f(this.val));
  }
}
Enter fullscreen mode Exit fullscreen mode

In the above code, Functor is a functor. Its map method accepts the function f as a parameter and then returns a new functor. The value contained in it is the one processed by f (f(this.val)).

It is generally agreed that the sign of a functor is that the container has a map method. This method maps each value inside the container to another container.

The following are some usage examples:

(new Functor(2)).map(function (two) {
  return two + 2;
});
// Functor(4)

(new Functor('flamethrowers')).map(function(s) {
  return s.toUpperCase();
});
// Functor('FLAMETHROWERS')

(new Functor('bombs')).map(_.concat(' away')).map(_.prop('length'));
// Functor(10)
Enter fullscreen mode Exit fullscreen mode

The above examples show that the operations in functional programming are all completed through functors, that is, the operations are not directly on the values but on the containers of these values - the functors. The functor itself has an external interface (the map method), and various functions are operators. They are connected to the container through the interface, causing the values inside the container to be transformed.

Therefore, learning functional programming is actually learning various operations of functors. Since the operation methods can be encapsulated in the functor, various different types of functors have been derived. There are as many types of functors as there are operations. Functional programming becomes the application of different functors to solve practical problems.

IV. The of Method

You may have noticed that when generating a new functor above, the new command was used. This is really not like functional programming because the new command is a symbol of object - oriented programming.

It is generally agreed in functional programming that a functor has an of method to generate a new container.

The following replaces new with the of method:

Functor.of = function(val) {
  return new Functor(val);
};
Enter fullscreen mode Exit fullscreen mode

Then, the previous example can be changed to the following:

Functor.of(2).map(function (two) {
  return two + 2;
});
// Functor(4)
Enter fullscreen mode Exit fullscreen mode

This is more like functional programming.

V. Maybe Functor

The functor accepts various functions to process the values inside the container. Here comes a problem. The value inside the container may be a null value (such as null), and the external function may not have a mechanism to handle null values. If a null value is passed in, it is likely to cause an error.

Functor.of(null).map(function (s) {
  return s.toUpperCase();
});
// TypeError
Enter fullscreen mode Exit fullscreen mode

In the above code, the value inside the functor is null, and an error occurs when converting lowercase to uppercase.

The Maybe functor is designed to solve this kind of problem. Simply put, its map method has a null value check.

class Maybe extends Functor {
  map(f) {
    return this.val? Maybe.of(f(this.val)) : Maybe.of(null);
  }
}
Enter fullscreen mode Exit fullscreen mode

With the Maybe functor, handling null values will not cause an error.

Maybe.of(null).map(function (s) {
  return s.toUpperCase();
});
// Maybe(null)
Enter fullscreen mode Exit fullscreen mode

VI. Either Functor

The conditional operation if...else is one of the most common operations. In functional programming, the Either functor is used to express it.

The Either functor has two values inside: the left value (Left) and the right value (Right). The right value is the value used under normal circumstances, and the left value is the default value used when the right value does not exist.

class Either extends Functor {
  constructor(left, right) {
    this.left = left;
    this.right = right;
  }

  map(f) {
    return this.right? 
      Either.of(this.left, f(this.right)) :
      Either.of(f(this.left), this.right);
  }
}

Either.of = function (left, right) {
  return new Either(left, right);
};
Enter fullscreen mode Exit fullscreen mode

The following is the usage:

var addOne = function (x) {
  return x + 1;
};

Either.of(5, 6).map(addOne);
// Either(5, 7);

Either.of(1, null).map(addOne);
// Either(2, null);
Enter fullscreen mode Exit fullscreen mode

In the above code, if the right value has a value, the right value is used; otherwise, the left value is used. In this way, the Either functor expresses the conditional operation.

A common use of the Either functor is to provide a default value. The following is an example:

Either
.of({address: 'xxx'}, currentUser.address)
.map(updateField);
Enter fullscreen mode Exit fullscreen mode

In the above code, if the user does not provide an address, the Either functor will use the default address in the left value.

Another use of the Either functor is to replace try...catch, using the left value to represent an error.

function parseJSON(json) {
  try {
    return Either.of(null, JSON.parse(json));
  } catch (e: Error) {
    return Either.of(e, null);
  }
}
Enter fullscreen mode Exit fullscreen mode

In the above code, if the left value is empty, it means there is no error; otherwise, the left value will contain an error object e. Generally speaking, all operations that may cause errors can return an Either functor.

VII. Ap Functor

The value contained in a functor may well be a function. We can imagine a situation where the value of one functor is a number, and the value of another functor is a function.

function addTwo(x) {
  return x + 2;
}

const A = Functor.of(2);
const B = Functor.of(addTwo)
Enter fullscreen mode Exit fullscreen mode

In the above code, the value inside functor A is 2, and the value inside functor B is the function addTwo.

Sometimes, we want the function inside functor B to be able to use the value inside functor A for calculation. This is where the Ap functor comes in.

ap is the abbreviation of "applicative". Any functor that deploys the ap method is an Ap functor.

class Ap extends Functor {
  ap(F) {
    return Ap.of(this.val(F.val));
  }
}
Enter fullscreen mode Exit fullscreen mode

Note that the parameter of the ap method is not a function but another functor.

Therefore, the previous example can be written as follows:

Ap.of(addTwo).ap(Functor.of(2))
// Ap(4)
Enter fullscreen mode Exit fullscreen mode

The significance of the Ap functor is that for functions with multiple parameters, values can be taken from multiple containers to achieve chained operations of functors.

function add(x) {
  return function (y) {
    return x + y;
  };
}

Ap.of(add).ap(Maybe.of(2)).ap(Maybe.of(3));
// Ap(5)
Enter fullscreen mode Exit fullscreen mode

In the above code, the function add is in the curried form and requires two parameters in total. Through the Ap functor, we can take values from two containers. It also has another way of writing:

Ap.of(add(2)).ap(Maybe.of(3));
Enter fullscreen mode Exit fullscreen mode

VIII. Monad Functor

A functor is a container that can contain any value. It is completely legal for a functor to contain another functor. However, this will result in a nested functor.

Maybe.of(
  Maybe.of(
    Maybe.of({name: 'Mulburry', number: 8402})
  )
)
Enter fullscreen mode Exit fullscreen mode

This functor has three Maybe nested. If you want to get the internal value, you have to take this.val three times in a row. This is of course very inconvenient, so the Monad functor emerged.

The role of the Monad functor is to always return a single - layer functor. It has a flatMap method, which has the same function as the map method. The only difference is that if a nested functor is generated, it will extract the value inside the latter to ensure that what is returned is always a single - layer container and there will be no nested situation.

class Monad extends Functor {
  join() {
    return this.val;
  }
  flatMap(f) {
    return this.map(f).join();
  }
}
Enter fullscreen mode Exit fullscreen mode

In the above code, if the function f returns a functor, then this.map(f) will generate a nested functor. Therefore, the join method ensures that the flatMap method always returns a single - layer functor. This means that the nested functor will be flattened.

IX. IO Operations

An important application of the Monad functor is to implement I/O (input/output) operations.

I/O is an impure operation, and ordinary functional programming cannot handle it. At this time, the I/O operation needs to be written as a Monad functor to complete the operation.

var fs = require('fs');

var readFile = function(filename) {
    return new IO(function() {
        return fs.readFileSync(filename, 'utf - 8');
    });
};

var print = function(x) {
    return new IO(function() {
        console.log(x);
        return x;
    });
}
Enter fullscreen mode Exit fullscreen mode

In the above code, file reading and printing are impure operations themselves, but readFile and print are pure functions because they always return the IO functor.

If the IO functor is a Monad with a flatMap method, then we can call these two functions as follows:

readFile('./user.txt')
  .flatMap(print)
Enter fullscreen mode Exit fullscreen mode

The amazing thing is that the above code completes an impure operation, but because flatMap returns an IO functor, this expression is pure. We complete an operation with side - effects through a pure expression, which is the role of the Monad.

Since what is returned is still an IO functor, chained operations can be achieved. Therefore, in most libraries, the flatMap method is renamed chain.

var tail = function(x) {
    return new IO(function() {
        return x[x.length - 1];
    });
}

readFile('./user.txt')
  .flatMap(tail)
  .flatMap(print)

// Equivalent to
readFile('./user.txt')
  .chain(tail)
  .chain(print)
Enter fullscreen mode Exit fullscreen mode

The above code reads the user.txt file and then selects and outputs the last line.

Leapcell: The Next - Gen Serverless Platform for Web Hosting

Image description

Finally, I would like to recommend the best platform for deploying services: Leapcell

1. Multi - Language Support

  • Develop with JavaScript, Python, Go, or Rust.

2. Deploy unlimited projects for free

  • Pay only for usage — no requests, no charges.

3. Unbeatable Cost Efficiency

  • Pay - as - you - go with no idle charges.
  • Example: $25 supports 6.94M requests at a 60ms average response time.

4. Streamlined Developer Experience

  • Intuitive UI for effortless setup.
  • Fully automated CI/CD pipelines and GitOps integration.
  • Real - time metrics and logging for actionable insights.

5. Effortless Scalability and High Performance

  • Auto - scaling to handle high concurrency with ease.
  • Zero operational overhead — just focus on building.

Image description

Explore more in the documentation!

Leapcell Twitter: https://x.com/LeapcellHQ

Top comments (2)

Collapse
 
dariomannu profile image
Dario Mannu

I like how you've given a causal explanation for the reason monads were created (flatMap). Oftentimes you're just taught they exist, use them.

Anyway, I think you could have explained your own starting question a bit in more detail: why use functional programming at all, as @pengeszikra is also wondering.

The big deal here is that with FP you can write really high quality applications, with a lot less lines of code to write (and maintain, and refactor, and look after) compared to imperative equivalents.
It also guarantees a reduced number of bugs and requires less unit test cases for an equival level of coverage.

I would also add that when we talk front-end, the advantages are even more apparent in functional-reactive programming. As we apply these same concepts to the UI through libraries such as rxjs and rimmel.js, they help turning any intricated, multi-state, multi-step sync+async UI trouble (as in... pretty much everything you on the front-end) into something trivial to implement in practice.

The resulting code is neat, concise, logical, something you look at and you immediately realise it's sound, it's reliable, it will scale perfectly as your applications grow. I love that!

Collapse
 
pengeszikra profile image
Peter Vivo

Why sacrifice a functional programming so much?
Just for say at the end: this function is pure?