Avatar LogoJeff Thomas

Functional Programming Series: Fundamentals

written byJeff Thomas

Functional Programming|Series|TypeScript

Published: May 1, 2023

30 min read |
Functional Programming Series: Fundamentals

Photo by: Wesley Tingey on Unsplash

Introduction

Hello again, and welcome back to my blog series on Functional Programming in TypeScript! If you haven't already, I encourage you to check out the first post where I introduced the core concepts, history, and benefits of functional programming.

In this second post, I'll be guiding you through the fundamental principles that underpin functional programming. While some of the material we'll cover may seem basic at first glance, it's crucial to establish a strong foundation in these principles before progressing to more advanced topics.

By the end of this post, you'll have a deeper understanding of functional programming and how to apply its concepts in TypeScript. Armed with this knowledge, you'll be better equipped to write cleaner, more efficient, and more maintainable code.

So, let's continue our journey together and explore the fascinating world of functional programming in TypeScript!

JavaScript is the only language that I'm aware of that people feel they don't need to learn before they start using it.

Douglas Crockford

Programming Values

Functional programming treats everything as a value. Functional programming's approach to treating everything as a value can be seen as a significant departure from the object-oriented paradigm, where the focus is primarily on objects and their interactions. In object-oriented programming, we create classes, which are essentially blueprints for objects, and then instantiate those classes to create objects. These objects have state (properties) and behavior (methods), and they interact with each other to perform complex tasks.

On the other hand, functional programming emphasizes the use of values to accomplish the same goals. In this paradigm, we work with data in its simplest form, such as primitive values (e.g., numbers, strings, and booleans) and composite values (e.g., arrays, tuples, and records). Functions, which are also considered values, are used to transform and manipulate these data values. By treating everything as a value, functional programming encourages a more straightforward and declarative approach to problem-solving.

Values

Values are the most atomic entities in TypeScript and serve as the fundamental building blocks for our programs. Each value is a member of a specific type, such as a string, number, boolean, or more complex types. For example, the value hello is a member of the string type, while the value 5 belongs to the number type. As we delve deeper into functional programming, it's essential to understand that every element we work with, whether simple or complex, is a value that belongs to a particular type. Below are examples of values in TypeScript and their corresponding types.

// literal string
'jeff'
 
// literal boolean
false
 
// literal number
;(5)[
  // number[]
  (1, 2, 3)
]
 
// object
{
  foo: 'bar'
}

Expressions

Expressions are generalized values that have not yet been evaluated or normalized to their atomic form. They represent computations or operations that, when evaluated, produce a value. Expressions are evaluated before they are passed to or returned from functions. Below are various expressions and the process of evaluating them to obtain values.

'foo' + 'bar'
// Output: 'foobar'
 
1 + 1
// Output: 2
 
true ||
  false[
    // Output: true
 
    (1, 2, 3)
  ][0](
    // Output: 1
 
    { foo: 'bar' }
  ).foo
// Output: bar

Functions

Functions can also be regarded as values in TypeScript. The capability to pass functions as input arguments, return functions as output from another function, and assign functions to variables and object properties demonstrates their first-class status in the language. In essence, functions represent a type of generalized expression, which itself is a generalized value. This underscores the key principle in functional programming that functions are, in fact, treated as values.

Sometimes, the elegant implementation is just a function. Not a method. Not a class. Not a framework. Just a function.

John Carmack

Functions, which can be named or anonymous, represent generalized expressions. They are evaluated only when needed and can be reused throughout the program. More specifically, pure functions are considered values in functional programming. JavaScript provides various ways to create and utilize these functions as values, further emphasizing their flexibility and importance in the language.

// Function Declaration
// Note: This is the only one that supports hoisting.
function addOne(x: number) {
  return x + 1
}
 
// Function Expression
const addOne = function (x: number) {
  return x + 1
}
 
// Arrow Function
const addOne = (x: number) => x + 1

Watch Anjana Vakil deliver a fun talk on The universe in a single arrow: A live dive into lambda calculus that compares arrow functions in JavaScript to lambda calculus.

Pure Functions

The best thing about JavaScript is its implementation of functions. It got almost everything right. But, as you should expect with JavaScript, it didn’t get everything right.

JavaScript: The Good Parts by Douglas Crockford

Not all functions are actually values. Pure functions are a specific category of functions that qualify as values in functional programming. A function is considered pure if it meets the following criteria: given the same input, it always returns the same output and produces no side effects. Pure functions solely rely on their inputs to generate a result, without altering those inputs or depending on any external or global shared state. In essence, functions represent data as expressions that evaluate to a value, either immediately or later when needed.

From a mathematical perspective, a function is deemed pure if it adheres to these rules:

  • Total: There is a corresponding output for every input.
  • Deterministic: The same output is consistently produced for a given input.
  • Referential Transparency: The only observable effect is the computation of a value, with no additional side effects.

If a function declaration includes an impure expression but the effect is not visible outside the function scope, calling it can be still considered a pure expression. If an otherwise pure function depends on an impure one it is also impure, because impurity acts systemic.

Scriptum

There are several examples of functions that are not considered pure, such as writing to the console, accessing variables outside of their scope, utilizing random number generation methods, or calling external APIs. These functions produce side effects, interact with external state, or have unpredictable outcomes, making them impure. In contrast, pure functions can be thought of as Lego building blocks - they are modular, composable, and self-contained, which allows us to create more robust and maintainable code. By using pure functions, we can reason about our code more easily, reduce the likelihood of bugs, and build more efficient and scalable applications.

The functional way in JavaScript involves these four simple rules:

  • Functions must always return a value and (with a few exceptions) declare at least one parameter.
  • The observable state of an application before and after a function runs does not change; it's immutable and side-effect-free. A new state is created each time.
  • Everything a function needs to carry its work must be passed in via arguments or inherited from its surrounding outer function (closure), provided that the outer function abides by the same rules.
  • A function called with the same input must always produce the same output. This rule leads to a principle known as referential transparency, which states that an expression and its corresponding value are interchangeable without altering the code's behavior.

The Joy of JavaScript by Luis Atencio

Totality

In functional programming, a pure function is considered mathematically total when it guarantees a corresponding output for every possible input. This means that for each input value within the function's domain, there is a well-defined output value in its codomain. Total functions are predictable and help prevent runtime errors, making it easier to reason about the code.

Functions should do one thing. They should do it well. They should do it only.

Clean Code by Robert C. Martin

In programming terms, this refers to the relationship between the inputs and outputs of a function. The domain represents the set of all possible input values that the function can accept, while the codomain represents the set of all possible output values that the function can produce. A well-defined output value means that, for any given valid input, the function will return a valid output belonging to the codomain without any unexpected behavior or errors.

Let's take a look at a TypeScript example to clarify this concept:

type Domain = string | null
type Codomain = string
 
function greetUser(name: Domain): Codomain {
  if (name === null) {
    return 'Hello, Guest!'
  }
  return `Hello, ${name}!`
}

In this example, the greetUser function has a domain of type Domain, which is defined as string | null. This means the function can accept any string value or a null value as input. The codomain of the function is of type Codomain, which is defined as string. This indicates that the function will always return a string value as output.

For any input value within the domain (any string or null), the greetUser function will produce a well-defined output value in its codomain (a string). This relationship between input and output values makes the function predictable and less prone to errors.

Partial Functions

In contrast, a partial function is one that can only return a result for a subset of the inputs it can be given and would result in an error or exception being thrown or return an undefined output for certain inputs outside of the original type. This stands in contrast to a total function, which is able to return a result for every possible input it can be given.

Here's an example of a non-total function:

// Partial Function (Non-Total)
function unsafeGreetUser(name: string | null): string {
  if (name === null) {
    throw new Error('Name cannot be null.')
  }
  return `Hello, ${name}!`
}
 
console.log(unsafeGreetUser('Alice')) // Output: "Hello, Alice!"
console.log(unsafeGreetUser(null)) // Throws Error: "Name cannot be null."

In this case, unsafeGreetUser is a partial function because it throws an error when the input is null. This makes the function less predictable and more prone to errors, highlighting the importance of ensuring total functions in functional programming.

Let's examine a more common TypeScript example that demonstrates the total requirement using both a pure function and an impure function:

// Pure Function
function safeDivide(numerator: number, denominator: number): number | null {
  if (denominator === 0) {
    return null
  }
  return numerator / denominator
}
 
// Impure Function
function unsafeDivide(numerator: number, denominator: number): number {
  return numerator / denominator
}
 
// Test Total Function
console.log(safeDivide(10, 2)) // Output: 5
console.log(safeDivide(10, 0)) // Output: null
 
// Test Partial Function
console.log(unsafeDivide(10, 2)) // Output: 5
console.log(unsafeDivide(10, 0)) // Output: Infinity

In this example, safeDivide is a pure function that handles the edge case of dividing by zero and returns null for that particular input. As a result, it satisfies the total requirement, providing a corresponding output for every possible input. On the other hand, unsafeDivide is an impure function that does not handle the division by zero edge case properly, and returns Infinity in such a scenario. This makes the function less predictable and potentially prone to errors, demonstrating the importance of ensuring total functions in our code.

This example is still somewhat problematic as we are using conditionals to check for 0. We should strive to better model our types that define the valid values and push these checks to the edges (e.g., inputs and outputs) of the system. Check out Eric Normand's post on What is a total function?.

Deterministic Behavior

In functional programming, a pure function exhibits both determinism and totality. Determinism means that for a given input, the function consistently produces the same output, while totality refers to the property that there is a corresponding output for every input within the function's domain. These concepts come from mathematical functions, which are well-defined mappings from their domain (set of inputs) to their codomain (set of possible outputs). In mathematics, a function is total if it has a defined output for every input in its domain.

Understanding functions in this way—as a mapping of a set of inputs to a set of outputs—is crucial to understanding functional programming.

Haskell Programming: From First Principles by Christopher Allen and Julie Moronuki

Deterministic functions in programming provide a consistent output for every input, relying solely on their input arguments to compute the result, without depending on external state or side effects. Total functions, on the other hand, ensure that for each input value within the function's domain, there is a valid output value in its codomain, without any unexpected behavior or errors. The combination of determinism and totality in pure functions results in predictable and reliable behavior, making it easier to reason about the code and reducing the likelihood of bugs.

The first rule of functions is that they should be small. The second rule of functions is that they should be smaller than that.

Clean Code by Robert C. Martin

Let's explore a TypeScript example that demonstrates the concept of determinism using a pure function:

// Pure Function: Deterministic
function add(a: number, b: number): number {
  return a + b
}
 
console.log(add(3, 5)) // Output: 8
console.log(add(3, 5)) // Output: 8

In this example, add is a pure and deterministic function. Given the same input values of 3 and 5, the function consistently produces the same output value of 8. This predictability is a desirable property of pure functions in functional programming.

On the other hand, an example of a non-deterministic function would be one that produces different outputs for the same input, usually due to external factors or randomness. Here's an example of a non-deterministic function:

// Non-Deterministic Function
function getRandomNumberInRange(min: number, max: number): number {
  return Math.floor(Math.random() * (max - min + 1)) + min
}
 
console.log(getRandomNumberInRange(1, 10)) // Output: Random number between 1 and 10
console.log(getRandomNumberInRange(1, 10)) // Output: Different random number between 1 and 10

In this case, getRandomNumberInRange is a non-deterministic function because it returns different output values for the same input values of 1 and 10. This unpredictability makes it harder to reason about the code, emphasizing the importance of striving for deterministic functions in functional programming.

Deterministic and total functions are essential characteristics of pure functions in functional programming. Determinism guarantees the same output for a given input, and totality ensures a valid output for every input within the domain. These properties simplify code reasoning, improve testability, and reduce the potential for errors by providing predictable and well-defined behavior.

Referential Transparency

Referential transparency is a core principle in functional programming that greatly contributes to the readability, maintainability, and testability of code. A function is considered referentially transparent if it consistently produces the same output for the same input, with no side effects. This means that any call to a referentially transparent function can be replaced with its resulting value without changing the behavior of the program. In essence, referentially transparent functions behave like mathematical functions, where the output is solely determined by the input values.

Let's explore a TypeScript example that demonstrates the concept of referential transparency using a pure function:

// Referentially Transparent
function pureIncrement(x: number): number {
  return x + 1
}
 
console.log(pureIncrement(5)) // Output: 6
console.log(pureIncrement(5)) // Output: 6

This pureIncrement function takes a single input x of type number and returns a new number that is the result of incrementing x by 1. The function is referentially transparent because it has no side effects and consistently produces the same output for the same input. Given a number x, calling pureIncrement(x) will always yield the same result, and you can replace any call to pureIncrement(x) with x + 1 without altering the behavior of the program.

On the other hand, an example of a non-referentially transparent function would be one that produces side effects, such as modifying external state or printing to the console. Here's an example of a non-referentially transparent function:

let counter = 0
 
// Non-Referentially Transparent Function
function impureIncrement(): number {
  counter += 1
  return counter
}
 
console.log(counter) // Output: 0
console.log(impureIncrement()) // Output: 1
console.log(impureIncrement()) // Output: 2
console.log(counter) // Output: 2

In this example, the impureIncrement function increments the global variable counter by 1 and returns its new value. The function is impure and not referentially transparent because it relies on and mutates external state. The output of impureIncrement depends on the current value of the global counter variable, so calling this function with the same input (or no input, in this case) can produce different results depending on the state of counter.

The problem with shared state is that in order to understand the effects of a function, you have to know the entire history of every shared variable that the function uses or affects.

Composable Software by Eric Elliot

This reliance on external state makes the function harder to reason about, test, and reuse. The presence of side effects (the mutation of the counter variable) introduces potential issues when working with concurrent or parallel code, as race conditions or other synchronization problems may arise. Such impure functions are not ideal in functional programming, where the goal is to create referentially transparent, pure functions that only depend on their input arguments and have no side effects.

Referential transparency, determinism, and totality are all essential properties of a pure function. While referential transparency ensures the absence of side effects, determinism guarantees that a function consistently produces the same output for a given input. Totality, on the other hand, asserts that there is a corresponding output for every input within the function's domain. In both mathematical and programming terms, a pure function must exhibit all three of these properties to be considered truly pure. The combination of referential transparency, determinism, and totality ensures that pure functions have predictable and well-defined behavior, making it easier to reason about the code and reducing the potential for errors.

Immutability

All race conditions, deadlock conditions, and concurrent update problems are due to mutable variables.

Clean Architecture by Robert C. Martin

Immutability is a crucial concept in functional programming, which is rooted in mathematical principles. In mathematics, when an expression is evaluated, the result is a value that remains constant and does not change over time. Similarly, immutability in functional programming refers to the idea that data should not be changed or modified once it has been created. Instead, new data is derived from existing data through the use of pure functions, which don't mutate their inputs and don't produce any side effects. Emphasizing immutability helps create more predictable, maintainable, and robust code.

Mutation can be harmful in several ways:

  1. It can lead to unpredictable behavior and make the code harder to reason about, as multiple parts of the application might change the same shared mutable state.
  2. It can introduce bugs related to concurrency and synchronization, as race conditions can occur when multiple parts of the code try to access and modify the shared mutable state simultaneously.
  3. It can make it difficult to trace the origin of a bug, as changes to shared mutable state can propagate across various parts of the application.

To promote immutability, you can use techniques like creating new objects or arrays instead of modifying existing ones. Here's an example with an array in TypeScript:

const originalArray = [1, 2, 3]
 
// Immutable operation - creates a new array with the original values plus 4
const newArray = originalArray.concat(4)
 
console.log(originalArray) // Output: [1, 2, 3]
console.log(newArray) // Output: [1, 2, 3, 4]

In this example, the originalArray remains unchanged when we create the newArray. The concat method creates a new array by combining the original array with the new element. This operation is immutable, as it does not modify the original data.

Values are immutable in pure functional programming languages. As a result, you typically don't encounter variables in such languages, as their existence would imply the possibility of change. Instead, pure functional programming languages emphasize the use of constant values and immutable data structures, promoting a programming style that is more predictable and easier to reason about.

However, TypeScript is not a pure functional language, so you will encounter variables. In TypeScript, using const to declare a variable does not guarantee the immutability of the underlying object. While const ensures that the variable binding itself cannot be reassigned, it doesn't prevent the object's properties from being changed. Here's an example to illustrate this:

const person = {
  name: 'Jeff',
  age: 42,
}
 
person.age = 31 // This is allowed
 
console.log(person) // Output: { name: 'Jeff', age: 31 }

In this example, even though person is declared with const, we can still change its age property. This demonstrates that const does not create an immutable object.

One way to make an object immutable in TypeScript is to use Object.freeze(). When an object is frozen, you cannot add, modify, or delete its properties. Here's an example:

const person = Object.freeze({
  name: 'Jeff',
  age: 42,
})
 
person.age = 31 // TypeError: Cannot assign to read-only property 'age' of object
 
console.log(person) // Output: { name: 'Jeff', age: 42 }

In this example, after freezing the person object, attempting to change the age property results in a TypeError. This makes the person object truly immutable.

However, keep in mind that Object.freeze() only creates a shallow freeze. If the object contains nested objects, their properties can still be modified. To create a deep freeze, you would need to recursively apply Object.freeze() to all nested objects, or use a library that provides deep freezing functionality.

Several third-party libraries can help enforce immutability in JavaScript and TypeScript. One such library is Immutable.js, which provides immutable data structures such as List, Map, and Set. Another library, Immer, allows you to work with mutable drafts while producing an immutable output.

Here's an example using Immer to update an object's property immutably:

import produce from 'immer'
 
const originalObject = {
  name: 'Jeff',
  age: 42,
}
 
// Immutable update with Immer
const updatedObject = produce(originalObject, (draft) => {
  draft.age = 31
})
 
console.log(originalObject) // Output: { name: 'Jeff', age: 42 }
console.log(updatedObject) // Output: { name: 'Jeff', age: 31 }

In this example, the produce function from Immer creates a mutable draft of the originalObject. Inside the function, we update the age property of the draft, which doesn't affect the original object. After the function completes, Immer produces a new, updated, and immutable object.

While functional programming emphasizes the use of pure functions and immutable data structures, it's still possible to handle side effects in a controlled and structured manner. In later posts of this series, we will explore how functional programming languages and techniques can elegantly manage side effects while preserving the core principles of functional programming.

While functions being unable to change state is good because it helps us reason about our programs, there's one problem with that. If a function can't change anything in the world, how is it supposed to tell us what it calculated? In order to tell us what it calculated, it has to change the state of an output device (usually the state of the screen), which then emits photons that travel to our brain and change the state of our mind, man.

Learn You a Haskell for Great Good! by Miran Lipovača

By embracing immutability in functional programming, we can create code that is more predictable, easier to reason about, and less prone to bugs related to shared mutable state. Additionally, immutability simplifies the development of concurrent or parallel code, leading to more robust, maintainable, and efficient software.

Check out Anjana Vakil's talk at RuhrJS Conference in 2017 titled, Immutable data structures for functional JS.

Properties

As you delve into functional programming, you may be surprised to find that some mathematical properties you learned in elementary school become quite relevant. While you don't need an extensive background in advanced math to learn functional programming, being aware of these properties can help you understand the underlying principles of this programming paradigm better.

Functional programming leans on several basic mathematical properties, which contribute to its elegance and power. Here are a few common examples:

  1. Associative property: This property states that changing the order of parentheses when adding or multiplying three different numbers will result in the same answer. From a mathematical perspective, the associative property can be represented as (a + b) + c == a + (b + c). In functional programming terms, this can be expressed as add(add(a, b), c) == add(a, add(b, c)). This property emphasizes that the grouping of operations does not affect the final result.
  2. Commutative property: According to this property, adding or multiplying two or more numbers in any order will result in the same answer. The commutative property in mathematics states that a + b == b + a. In functional programming terms, this can be expressed as add(a, b) == add(b, a). This property emphasizes that the order of operands does not affect the final result when performing commutative operations like addition or multiplication.
  3. Identity property: This property highlights that adding 0 to any number will result in the same number, and multiplying any number by 1 will produce the same number. The identity property in mathematics highlights that a + 0 == a. In functional programming terms, this can be expressed as add(a, 0) == a.
  4. Distributive property: This property allows us to simplify expressions by removing parentheses. It states that adding or subtracting two numbers inside parentheses, then multiplying the sum or difference by a number outside the parentheses, is equal to first multiplying the number outside the parentheses by each of the numbers inside the parentheses and then adding the two products together. The distributive property in mathematics can be represented as a(b + c) == ab + ac. In functional programming terms, this can be expressed as add(multiply(a, b), multiply(a, c)) == multiply(a, add(b, c)).

These properties hold true in functional programming and enable powerful features such as local and equational reasoning, which can make understanding large codebases much more manageable.

Local reasoning means we can think about an expression which sticks deep inside the structure and logic of our program without having to be concerned about this very context. With the absence of side effects we can focus on the piece of code we are interested in no matter how extensive the program becomes. Local reasoning is complemented by equational reasoning, which describes thinking about code using algebraic properties, which results in principled code changes that work as predicted.

Scriptum

Idempotency

Idempotency is an important concept in functional programming, which is often compared to referential transparency. While all referentially transparent functions are idempotent, the reverse is not necessarily true. An idempotent function may produce a side effect the first time it runs, but subsequent calls won't change the result or generate additional side effects. In simpler terms, idempotency means that an action can be repeated without altering the outcome.

Idempotence is orthogonal to purity, which means that idempotent functions can be either pure or impure. Similarly, pure functions can be either idempotent or non-idempotent. An algebra is considered idempotent if it follows the idempotent property, such as f(x) === f(f(x)). Idempotent functions are highly desirable in functional programming, as they offer predictability and stability.

Let's take a look at some TypeScript examples demonstrating idempotency and purity. This first example demonstrates a function that is both idempotent and pure:

// Idempotent and Pure
const toLower = (x) => x.toLowerCase()
 
toLower(toLower('HELLO')) == toLower('HELLO') // Output: true

In this example, the toLower function is both idempotent and pure, as calling it multiple times with the same input produces the same output without any side effects.

The next example demonstrates an idempotent function that is not pure, as it has side effects resulting from state mutation:

type PersonObject = {
  firstName: string
  lastName?: string
}
 
const deleteLastName = (o: PersonObject): PersonObject => {
  delete o.lastName
  return o
}
 
const person1: PersonObject = { firstName: 'John', lastName: 'Doe' }
const person2: PersonObject = { firstName: 'Jane', lastName: 'Doe' }
 
const modifiedPerson1 = deleteLastName(person1)
const modifiedPerson2 = deleteLastName(deleteLastName(person2))
 
console.log(person1) // Output: { firstName: 'John' }
console.log(person2) // Output: { firstName: 'Jane' }
console.log(modifiedPerson1) // Output: { firstName: 'John' }
console.log(modifiedPerson2) // Output: { firstName: 'Jane' }

In this example, the deleteLastName function removes the lastName property from a PersonObject and returns the modified object. The function is idempotent because calling it multiple times with the same input produces the same output, as shown with person2. However, it is impure because it mutates the original input object, which is a side effect. The person1 and person2 objects are directly modified by the function, violating the principle of immutability in functional programming.

The next example demonstrates a pure function that is not idempotent, as it has different results when repeated:

// Non-Idempotent (different result when repeated) and Pure
const inc = (x: number) => x + 1
inc(inc(0)) === inc(0) // Output: false

The inc function is non-idempotent, as calling it multiple times with the same input produces different results. However, it's still a pure function, as it doesn't cause any side effects.

The next example is neither pure or idempotent:

// Non-Idempotent (different result when repeated) and Not Pure (side effects)
const log = (x: string) => {
  console.log(x)
  return x
}
 
log(log('foo')) // Output: logs twice
log('foo') // Output: logs once

Finally, the log function is neither idempotent nor pure. It produces side effects by logging the input value to the console, and calling it multiple times generates different results.

These examples showcased the various combinations of idempotent, non-idempotent, pure, and impure functions, and how they behave in different situations.

Higher Order Functions

Higher-order functions are a fundamental concept in functional programming, allowing developers to create reusable and composable building blocks. These functions can take other functions as arguments, return functions as results, or both, enabling more abstract and modular code.

In TypeScript, functions are first-class citizens, which means they can be treated like any other variable: assigned to variables, passed as arguments to other functions, returned as values from other functions, and even possess properties and methods. This first-class status allows for the powerful capabilities of higher-order functions.

Higher-order functions are particularly useful for promoting reusability and abstraction. They enable developers to create generic, adaptable functions that can be applied to various data types, such as objects, strings, or any other type. This flexibility stems from separating specific operations from the generic logic provided by the higher-order function.

By abstracting away repetitive patterns, higher-order functions make code more concise and easier to maintain. They allow developers to focus on a higher level of abstraction, emphasizing the problem's logic rather than low-level implementation details.

In this example, we'll see how higher-order functions enable reuse. We'll create a generic applyOperation function that takes an operation function as an argument and applies it to two numbers.

// Define a type for the operation function
type Operation = (a: number, b: number) => number
 
// Higher-order function that takes an operation function and two numbers as arguments
const applyOperation = (operation: Operation, a: number, b: number): number => {
  return operation(a, b)
}
 
// Some operation functions
const add = (a: number, b: number): number => a + b
const multiply = (a: number, b: number): number => a * b
 
// Usage
console.log(applyOperation(add, 5, 3)) // Output: 8
console.log(applyOperation(multiply, 5, 3)) // Output: 15

In this example, the applyOperation function is a higher-order function that takes an operation function (like add or multiply) and two numbers as arguments. The applyOperation function is reusable, and can work with any operation function that follows the Operation type. This demonstrates how higher-order functions enable code reuse and enhance the flexibility of our code in TypeScript.

Now that we've seen how higher-order functions enable reusability and flexibility in our TypeScript code, let's dive into some built-in JavaScript higher-order functions that are widely used in functional programming. These functions, such as map, filter, and reduce, are readily available for us to use and can significantly improve the readability and maintainability of our code.

The map function takes a function as its argument and applies it to each element of an array, creating a new array with the transformed elements.:

const numbers = [1, 2, 3, 4, 5]
const double = (x: number): number => x * 2
 
const doubledNumbers = numbers.map(double)
 
console.log(doubledNumbers) // Output: [2, 4, 6, 8, 10]

The filter function takes a predicate function (a function that returns a boolean) as its argument and filters the elements of an array based on the predicate. It returns a new array containing only the elements that satisfy the predicate.

const numbers = [1, 2, 3, 4, 5]
const isEven = (x: number): boolean => x % 2 === 0
 
const evenNumbers = numbers.filter(isEven)
 
console.log(evenNumbers) // Output: [2, 4]

The reduce function takes a reducer function and an optional initial value as its arguments. It applies the reducer function to each element of the array, accumulating a single output value.

const numbers = [1, 2, 3, 4, 5]
const sum = (accumulator: number, currentValue: number): number =>
  accumulator + currentValue
 
const total = numbers.reduce(sum, 0)
 
console.log(total) // Output: 15

reduce is an incredibly powerful higher-order function in functional programming. It can be used to define other essential functions like map and filter, as well as several others such as compose. This versatility makes reduce a core concept in functional programming.

By using higher-order functions like map, filter, and reduce, developers can write more expressive, concise, and maintainable code in a functional programming style.

Summary

In this blog post, we explored key concepts in functional programming, starting with programming values, where we discussed values, expressions, and functions. We then delved into pure functions, covering total and partial functions, deterministic behavior, and referential transparency. We highlighted the importance of immutability in functional programming and how it promotes safer, more predictable code. Finally, we examined higher-order functions, which enable greater abstraction and modularity, making code more reusable and maintainable. By understanding these fundamental principles, you can harness the power of functional programming in your projects, leading to cleaner, more robust, and more efficient code.

See the associated LinkedIn post.

main
git log
Comments

To leave feedback or questions, simply login using your preferred social network. I will read and answer your comments promptly, but please keep in mind that they will be public.

No comments yet.
main