Functional Paradigm: Concepts and Guidelines

In this lesson, we’ll take a look at the concepts of functional programming and their application in Clojure. Don’t worry about understanding the code yet; we’re going to be talking about all of it in the following lessons.

Immutability

Immutability is the main characteristic of functional programming. It consists of the principle of variables, which are read-only structures, meaning we’re not able to change the state of those variables during the execution of a system. Every time we feel like modifying a variable, immutability will take us to the creation of a new variable with the content of the previous one plus whatever we need to include or exclude.

Press + to interact
Immutable vs. mutable
Immutable vs. mutable

Furthermore, immutability has the remarkable ability to improve the consistency of services. This is because it makes the developers more aware of the state of the service and of what has been going on throughout the execution. So, testing and debugging are simpler because we have full control over the state of the service at each stage.

As we can see, immutability is the opposite of the imperative paradigm, and understanding this concept will guarantee a change in our mindset when we start coding.

Moreover, at this point, it’s important to start thinking about how to solve problems without using state (mutable) variables.

Clojure example

Press + to interact
; Defining a variable with the name amount and content 10
(def amount 10)
; Defining a variable with the name multiplier and content 2
(def multiplier 2)
; Now our amount should be "updated" to the multiplication of 10 with 2
(* amount multiplier)
; However, if we check the value of the amount, we will see that it was not updated
(println amount)
; So, the function of multiplication creates a new "variable",
; as we have immutability as the first principle.
(println (* amount multiplier))

First-class functions

First-class functions are functions that are treated as any other variable in the programming language. In such a language, a function can be passed as an argument to other functions, can be returned by another function, and can be assigned as a value to a variable.

Clojure example

Press + to interact
; Anonymous function defined as a variable
(def my-func (fn [] (println "Hello World")))
; Passing a function as an argument to another function, where the argument function is already defined.
(defn other-func [f] (f))
; Executing the other function passing my-func as an argument
(other-func my-func)
; Returning a function (without executing it) from another function
(defn another-func
[]
(println "This is the only print that you will see from the execution of another-func")
my-func)
; Executing another function
(another-func)

Higher-order functions

Higher-order functions are functions that perform operations over other functions. These functions perform the action of receiving a function as an argument or returning functions as a response.

Clojure example

This example is the same as the previous one. The higher-order functions in this example are other-func, which is receiving a function as an argument and executing it, and another-func, which is returning the function without executing it.

Press + to interact
; Anonymous function defined as a variable
(def my-func (fn [] (println "Hello World")))
; This is a higher-order function as it receives the f,
; which is a function.
(defn other-func [f] (f))
; Executing the other function passing my-func as an argument
(other-func my-func)
; This is a higher-order function as it returns a function
; (without executing it) from another function
(defn another-func
[]
(println "This is the only print that you will see from the execution of another-func")
my-func)
; Executing another function
(another-func)

Pure functions

Pure functions are functions that have a single possible return for an input. So we’re able to predict their result based on the input that we’re passing. They might be understood as a mathematical function. If we have x+2=yx + 2 = y and xx is 22, yy will always be 44. In any other situation, this wouldn’t be true.

Clojure example

Press + to interact
; We will define a function that will validate if an age passed
; to it is older than 21 and will retrieve a boolean.
(defn ofLegalAge? [age]
(> age 21))
; Let's check the possible results and print to see them
(println (ofLegalAge? 12))
(println (ofLegalAge? 40))

Function composition

Function composition is the process of composing the solution to a problem combining small units and functions with small responsibilities. It’s a flexible and powerful way of organizing code in which the input of a function comes from the output of the previous one.

Compared to object-oriented programming, in functional programming, function composition takes the place of inheritance when a class derives from another class, inheriting all the public and protected properties and methods from the parent class.

Function composition is closely related to a fundamental mindset shift, which involves the transformation of elements without associating them with variables and symbols. This approach contrasts with manipulating elements using auxiliary variables.

Clojure example

Press + to interact
; We need to perform a calculation that involves adding 10 to a value n,
; then subtracting 2, and finally formatting the result into a map {:value result}.
;Using function composition we split the problem into functions
;that will do it for us
(defn add-10 [a] (+ a 10))
(defn sub-2 [a] (- a 2))
(defn format-result [result] {:value result})
; Function composition
(defn calculate-and-format
[n]
(format-result (sub-2 (add-10 n))))
; Our input is 10
(println (calculate-and-format 10))

Expressions

Expressions are variables, symbols, or functions that have a value. In functional programming, because we do things with immutability and with functions as our main resource, we want to make sure that our functions are all expressions and have a return value. So, we try to avoid statement functions, which are functions with instructions, logic, and no return, which are pretty common in imperative paradigms.

Clojure example

Press + to interact
; Expression Function
(defn expression [a] (+ a 2))
; Statement Function
(defn statement [a]
(let [_ (+ a 2)]))
; Run of the two functions
(println "Expression run:"
(expression 2))
(println "Statement run:"
(statement 2))

Expressions allow us to do a much more precise test. Like in any mathematical expression, we’re able to know the exact result based on the variables received.