한국어 | English | 日本語
Senior Web Application Developer (8.8+ years)
Tech & Dev
engineering
Focusing on web frontend and backend development

A One-Page Guide to Functional Programming - Closures, Currying, Functor, Monad

Functional programming, a paradigm distinct from object-oriented programming, demands a shift in perspective towards data and functions rather than just a difference in syntax. This article explores why this philosophy—treating functions as variables and minimizing side effects—is regaining attention in modern development environments by examining its core concepts.
This article clarifies the conditions for first-class functions, which form the basis of functional programming, and the concept of pure functions, which ensure referential transparency. Furthermore, it covers currying and closures utilizing higher-order functions, and the definitions of Functor and Monad for safely handling data structures, along with the principles of function composition.

Functional Programming

Functional programming can be summarized in one sentence:

Functions must be usable ① as variables, ② as parameters, ③ as return values, and ④ must possess the characteristics of pure functions.

Function Pointers

Since functions are references, not values, using functions as first-class functions requires the use of function pointers.

void qsort (void* base, size_t n, size_t size,  int (*compare)(const void*,const void*));

The C language example above shows a compare function pointer being passed as the last parameter of the qsort algorithm. However, some argue that functions in C are not first-class functions but rather second-class functions, because they are pre-compiled rather than defined at runtime.

Lambda (Anonymous Function)

Lambda is a concept used in computer science and mathematical logic, representing the prototype of current programming functions.

It is a functional abstraction that takes input values and returns a result by utilizing free variables defined outside the function.

The fact that it only defines a function without executing it is identical to defining a function first in programming. As a concept in mathematical logic and the prototype of functions, lambdas have no function names. For this reason, lambdas are sometimes called anonymous functions in programming. The concept is clear, but when and why are lambdas used?

In programming, there are two ways to use values:

let defined: Int = 10;
print(defined);
print(10);

There are also two ways to use functions, similar to values:

const defined = (param: Int) => { return param; };
print(defined(10));
print(((param: Int) => { return param; })(10));

Lambdas pass function pointers as variables, parameters, and return values, similar to regular functions, but they differ in the timing of function definition, which offers advantages:

Through lambdas, functions can be used as first-class functions by defining them inline without the need for prior definition.

Function Objects

In object-oriented programming, a function cannot exist as a standalone entity; it must belong within a class. If you want to use a function as a lambda, you must create a function object and use it at the object level. In object-oriented programming, while lambdas may appear to exist as standalone functions, they are actually syntactic sugar for function objects, where an unnamed object wraps a single function.

Closures

Lambdas and closures may seem similar, but they are distinctly different concepts. Let’s look at their definitions:

A lambda refers to an anonymous function.
It is used when you want to directly use a function as a variable, parameter, or return value for one-time purposes.

A closure refers to a function that retains the environment (state) in which it was defined.
Here, the environment means the local variables within the scope where the closure is defined.

Typically, closures are often used by defining a function (closure) C inside another function A. If closure C is defined within function A, C can naturally reference and use A’s variables even without them being explicitly passed as parameters. This is the environment (state).

If we view closures as a way to use functions like objects, the reasons for using closures are similar to the reasons for using objects:

func query(dbName: String) -> (String) -> (Person) {
  let instance: DBInstance = DBConfig.getInstance(dbName)
  // * Inside the closure { }, the 'instance' variable, which exists within the function where the closure is defined, is used.
  return { (tableName: String) -> (Person) in 
    return instance.getTable(tableName).getFirst()
  }
}

Swift Closures

As seen above, while the definition of a closure is not an anonymous function, in Swift, closures are used without a name, effectively functioning as anonymous functions. Swift closures distinguish ‘parameters’ and ‘return statements’ with the in keyword.

Closure Swift Example

Swift closures can be abbreviated as much as desired:

{ (parameters) -> (return_type) in return /* statements using parameters */ }
{ parameters in return /* statesments using parameters */ }
{ parameters in /* statesments using parameters */ }
{ /* statesments using parameters with $0, $1 ... */ }
var sorted = sort(names, { $0 < $1 })
var sorted = sort(names) { $0 < $1 }

Higher-Order Functions

Higher-order functions utilize the second or third of the three first-class function conditions discussed earlier.

A higher-order function means a function that takes other functions as parameters or returns a function as its result.
Because it’s a function that operates on other functions, it’s called a higher-order function, implying a meta-function.

Currying

Currying utilizes the third of the three first-class function conditions.

Currying refers to a function returning another function. In Swift, currying is commonly used in a way where a function returns a closure.

func curringExample: (a: Int, b: Int, c: Int) -> (Int, Int) -> (Bool) { ... }

The curringExample above shows a function that takes parameters a, b, and c and returns another function which takes two Int parameters (Int, Int) and returns a Bool.

Even the way ‘an object of a class’ calls ‘a function of a class object’ in Swift uses currying.

let someInstance = SomeClass()
someInstance.someFunction(params: /* parameters */) 

The class method above is actually performed by passing the object to the class function as follows:

SomeClass.someFunction(self: someInstance)(params: /* parameters */) 

As a side note, Kotlin’s extension functions are also used in a similar manner, where the receiver object (class) is passed as a parameter to a function defined for that receiver type.

Functor

A Functor is a data structure. Let’s briefly look at functions before diving into the Functor concept.

Function = Mapping

A function takes Input A and produces Output B as a result. Alternatively, a function is a mapping between Input A and Output B.

Data Structure Mapping

If you apply a mapping to an entire data structure, you must apply the mapping to each individual element within that data structure. For example, if the data structure is a list, it goes through the following procedure by iterating 0, 1…:

Function Example

If we define the ability to apply a mapping function to each element as Mappable, then the list example can be defined as a Mappable data structure. The example in the figure above is a Functor example where each element is stringified from an Int data structure to a String data structure.

A Functor is a Mappable (possessing a mapping function) data structure.
Any data structure that can apply a mapping function to its individual elements can be called a Functor.

Functor Definition

For any ① data structure, if you want to apply a desired operation, you just need to define what type (T) the unit element inside the data structure is, and ② the Mapping for that unit element (T). If ① is viewed as a class property and ② as a class method, a Functor is sometimes called a Function Object.

The concept of Functor originates from Category Theory, where it refers to a mapping from one category to an identical category. This is conceptually identical to mapping individual unit elements within a data structure to new values while preserving the overall data structure. When the data structure (category) remains unchanged, and only the values are mapped, Category Theory defines this as natural transformation.

Haskell’s Functor

When you look for Functors, you will likely first encounter Haskell’s Functor concept, which is defined as a typeclass as follows. It is instantiated by specifying the data structure type as desired. In Swift-like syntax, it can be expressed as:

In Haskell, a Functor can be seen as a generic (③ S, ① T, ② R) abstract class that has a data structure type (③ S) and an abstract mapping function from an element (① T) to an element (② R). When defining fmap() or map() functions in Haskell, you define the abstract mapping function and inject the data structure you want to transform, and a new identical data structure with only its internal values changed is returned.

If you are a Java user, you might recall the map() function of Stream, which helps in understanding this.

Java’s Stream is, more precisely, a Monad. The reason is that its mapping function:

In a Functor, after extracting a unit element from the original data structure, applying the mapping, and then inserting the result element back into the data structure. In contrast, in a Monad, after extracting a unit element from the original data structure and applying the mapping, it returns the resulting data structure directly by inserting that element into a data structure. Because the function itself returns a data structure, you can continuously chain operations like Stream.map().map().map()... from the mapping function’s result.

Let’s explore why we perform ‘element-to-data structure mapping’ instead of ‘element-to-element mapping’ in the Monad section below.

Monad

Before summarizing what a Monad is in one sentence, let’s understand why Monads are necessary. Do you know the difference between a ‘programming function’ in a programming language and a ‘function’ in academia?

In higher mathematics and university courses, no function f(x) would ever throw an Exception in the middle of execution because an input value was incorrect. However, programming functions can throw Exceptions if their state becomes invalid during operation.

From the perspective of pure functions, throwing an Exception is defined as a Side-Effect, so functions that throw Exceptions are defined as impure functions.

If, instead of stopping mid-execution when an Exception occurs in a programming function, the failure state were returned along with the result, the Side-Effect would be eliminated. This is essentially making programming functions pure. To return both ① the state value and ② the function’s inherent result value together, a data structure to bundle them both seems necessary.

To make Functor’s Mapping function a pure function, we tried returning a data structure that includes both
① the state value where an Exception might occur and ② the result value, in the function’s output.

Functor Return Value

Although we made Functor’s Mapping function return a data structure, a problem arose: the returned data structure was wrapped once more by the Functor’s data structure.

This happens because a Functor extracts an internal element from its data structure, applies an operation to it, and then maps the result element back into the data structure.

Monad Return Value

To avoid unnecessary double-wrapping and return only the data structure containing the Exception state, we explicitly define an Unwrap function (called flatMap) that extracts the value from the current data structure before the Mapping function is performed. The Monad pattern then directly returns the ‘data structure’ that is the result of mapping this ‘internal element of the data structure’ obtained by flatMap.

A Monad is a Mappable data structure that includes an Unwrap (flatMap) function.
A Monad’s Mapping function returns a data structure that contains both ① the state value and ② the result value.

Monad Definition

Many explanations of Monads describe them as data types possessing both Context and Content. Context is often explained as a ‘state’ of having/not having a value, and Content as the ‘value’ or ‘result’ we want to operate on. While a Monad’s Context doesn’t necessarily have to represent a nullable state, because cases where functions might throw Exceptions often involve null values, many explanations tend to describe it in terms of nullability.

Function Composition

Monads not only ensure that the resulting data structure contains state values but also possess the property of function composition.

Monad Composition

This concludes our exploration of five core concepts in functional programming: closures, higher-order functions, currying, Functors, and Monads. If you have any questions or points for discussion, please let me know in the comments or personally. I especially want to thank a senior developer for helping me refine and improve the content of this article. In the next article, I will explain the reference cycle issues that arise when Swift closures reference external variables and the techniques to resolve them.


A One-Page Guide to Functional Programming - Closures, Currying, Functor, Monad
Author
Aaron
Posted on
Licensed Under
CC BY-NC-SA 4.0
CC BY-NC-SA 4.0
More in this category
Recent posts
The Erosion of Conversational Muscle and Communication Styles by LLM Filters
In an era where LLM tools, which filter out conversational impoliteness and deliver refined responses, have become commonplace, are we truly engaging in more thoughtful conversations? This article examines the phenomenon of conversational ability, which should be honed through countless failures in real-time communication, degenerating due to reliance on external tools. It further explores the potential societal anxieties and shifts in generational behavioral patterns that this trend may bring.
Optimal Timing and Strategy for Salary Negotiation with Senior Candidates
Salary negotiation is more than just an exchange of figures; it's a strategic dance of psychological timing. This analysis explores why engaging in a gradual negotiation process from the initial stages of recruitment, rather than waiting until after a final offer (when candidates tend to adopt a more calculative stance), proves more efficient for companies and fosters a more honest sharing of resources.
The Limits of the Rule of Law and Human Diversity
The belief that all human actions can be regulated by a single legal system may be an act of hubris. This article offers a sharp analysis of the paradox of the rule of law faced by humanity, which, having escaped the hierarchical controls of the Middle Ages, has now embraced infinite modern freedom. It further examines the deepening social coercion and the demonization of others that arise under the guise of diversity.
토스트 예시 메세지