about summary refs log tree commit diff stats
path: root/bash/talk-to-computer/corpus/programming/functional_programming.md
blob: 2572442ff78990ef5e1e8d914c47c0d2528b46ba (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# Functional Programming - A paradigm for declarative, predictable code

## Introduction

**Functional Programming (FP)** is a programming paradigm where software is built by composing **pure functions**, avoiding shared state, mutable data, and side-effects. It treats computation as the evaluation of mathematical functions. Instead of describing *how* to achieve a result (imperative programming), you describe *what* the result is (declarative programming).

This paradigm has gained significant traction because it helps manage the complexity of modern applications, especially those involving concurrency and complex state management. Programs written in a functional style are often easier to reason about, test, and debug.

## Core Concepts

### Pure Functions

A function is **pure** if it adheres to two rules:

1.  **The same input always returns the same output.** The function's return value depends solely on its input arguments.
2.  **It produces no side effects.** A side effect is any interaction with the "outside world" from within the function. This includes modifying a global variable, changing an argument, logging to the console, or making a network request.

<!-- end list -->

```javascript
// Pure function: predictable and testable
const add = (a, b) => a + b;
add(2, 3); // Always returns 5

// Impure function: has a side effect (console.log)
let count = 0;
const incrementWithLog = () => {
  count++; // And mutates external state
  console.log(`The count is ${count}`);
  return count;
};
```

### Immutability

**Immutability** means that data, once created, cannot be changed. If you need to modify a data structure (like an object or array), you create a new one with the updated values instead of altering the original. This prevents bugs caused by different parts of your application unexpectedly changing the same piece of data.

```javascript
// Bad: Mutating an object
const user = { name: "Alice", age: 30 };
const celebrateBirthdayMutable = (person) => {
  person.age++; // This modifies the original user object
  return person;
};

// Good: Returning a new object
const celebrateBirthdayImmutable = (person) => {
  return { ...person, age: person.age + 1 }; // Creates a new object
};

const newUser = celebrateBirthdayImmutable(user);
// user is still { name: "Alice", age: 30 }
// newUser is { name: "Alice", age: 31 }
```

### First-Class and Higher-Order Functions

In FP, functions are **first-class citizens**. This means they can be treated like any other value:

  * Assigned to variables
  * Stored in data structures
  * Passed as arguments to other functions
  * Returned as values from other functions

A function that either takes another function as an argument or returns a function is called a **Higher-Order Function**. Common examples are `map`, `filter`, and `reduce`.

```javascript
const numbers = [1, 2, 3, 4];
const isEven = (n) => n % 2 === 0;
const double = (n) => n * 2;

// `filter` and `map` are Higher-Order Functions
const evenDoubled = numbers.filter(isEven).map(double); // [4, 8]
```

## Key Principles

  - **Declarative Style**: Focus on *what* the program should accomplish, not *how* it should accomplish it. An SQL query is a great example of a declarative style.
  - **No Side Effects**: Isolate side effects from the core logic of your application. This makes your code more predictable.
  - **Function Composition**: Build complex functionality by combining small, reusable functions.
  - **Referential Transparency**: An expression can be replaced with its value without changing the behavior of the program. This is a natural outcome of using pure functions and immutable data.

## Implementation/Usage

The core idea is to create data transformation pipelines. You start with initial data and pass it through a series of functions to produce the final result.

### Basic Example

```javascript
// A simple pipeline for processing a list of users
const users = [
  { name: "Alice", active: true, score: 90 },
  { name: "Bob", active: false, score: 80 },
  { name: "Charlie", active: true, score: 95 },
];

/**
 * @param {object[]} users
 * @returns {string[]}
 */
const getHighScoringActiveUserNames = (users) => {
  return users
    .filter((user) => user.active)
    .filter((user) => user.score > 85)
    .map((user) => user.name.toUpperCase());
};

console.log(getHighScoringActiveUserNames(users)); // ["ALICE", "CHARLIE"]
```

### Advanced Example

A common advanced pattern is to use a reducer function to manage application state, a core concept in The Elm Architecture and libraries like Redux.

```javascript
// The state of our simple counter application
const initialState = { count: 0 };

// A pure function that describes how state changes in response to an action
const counterReducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 };
    case 'DECREMENT':
      return { ...state, count: state.count - 1 };
    case 'RESET':
      return { ...state, count: 0 };
    default:
      return state;
  }
};

// Simulate dispatching actions
let state = initialState;
state = counterReducer(state, { type: 'INCREMENT' }); // { count: 1 }
state = counterReducer(state, { type: 'INCREMENT' }); // { count: 2 }
state = counterReducer(state, { type: 'DECREMENT' }); // { count: 1 }

console.log(state); // { count: 1 }
```

## Common Patterns

### Pattern 1: Functor

A **Functor** is a design pattern for a data structure that can be "mapped over." It's a container that holds a value and has a `map` method for applying a function to that value without changing the container's structure. The most common example is the `Array`.

```javascript
// Array is a Functor because it has a .map() method
const numbers = [1, 2, 3];
const addOne = (n) => n + 1;
const result = numbers.map(addOne); // [2, 3, 4]
```

### Pattern 2: Monad

A **Monad** is a pattern for sequencing computations. Think of it as a "safer" functor that knows how to handle nested contexts or operations that can fail (like Promises or the `Maybe` type). `Promise` is a good practical example; its `.then()` method (or `flatMap`) lets you chain asynchronous operations together seamlessly.

```javascript
// Promise is a Monad, allowing chaining of async operations
const fetchUser = (id) => Promise.resolve({ id, name: "Alice" });
const fetchUserPosts = (user) => Promise.resolve([ { userId: user.id, title: "Post 1" } ]);

fetchUser(1)
  .then(fetchUserPosts) // .then acts like flatMap here
  .then(posts => console.log(posts))
  .catch(err => console.error(err));
```

## Best Practices

  - **Keep Functions Small**: Each function should do one thing well.
  - **Use Function Composition**: Use utilities like `pipe` or `compose` to build complex logic from simple building blocks.
  - **Embrace Immutability**: Use `const` by default. Avoid reassigning variables. When updating objects or arrays, create new ones.
  - **Isolate Impurity**: Side effects are necessary. Keep them at the boundaries of your application (e.g., in the function that handles an API call) and keep your core business logic pure.

## Common Pitfalls

  - **Accidental Mutation**: JavaScript objects and arrays are passed by reference, making it easy to mutate them accidentally. Be vigilant, especially with nested data.
  - **Over-Abstraction**: Don't use complex FP concepts like monad transformers if a simple function will do. Prioritize readability.
  - **Performance Misconceptions**: While creating many short-lived objects can have a performance cost, modern JavaScript engines are highly optimized for this pattern. Don't prematurely optimize; measure first.

## Performance Considerations

  - **Object/Array Creation**: In performance-critical code (e.g., animations, large data processing), the overhead of creating new objects/arrays in a tight loop can be significant.
  - **Structural Sharing**: Libraries like `Immer` and `Immutable.js` use a technique called structural sharing. When you "change" an immutable data structure, only the parts that changed are created anew; the rest of the structure points to the same old data, saving memory and CPU time.
  - **Recursion**: Deep recursion can lead to stack overflow errors. While some languages support **Tail Call Optimization (TCO)** to prevent this, JavaScript engines have limited support. Prefer iteration for very large data sets.

## Integration Points

  - **UI Frameworks**: FP concepts are central to modern UI libraries. **React** encourages pure components and uses immutable state patterns with Hooks (`useState`, `useReducer`).
    \-- **State Management**: Libraries like **Redux** and **Zustand** are built entirely on FP principles, particularly the use of pure reducer functions.
  - **Data Processing**: FP is excellent for data transformation pipelines. It's often used in backend services for processing streams of data.
  - **Utility Libraries**: Libraries like **Lodash/fp** and **Ramda** provide a rich toolkit of pre-built, curried, and pure functions for everyday tasks.

## Troubleshooting

### Problem 1: Debugging composed function pipelines

**Symptoms:** A chain of `.map().filter().reduce()` produces an incorrect result, and it's hard to see where it went wrong.
**Solution:** Break the chain apart. Log the intermediate result after each step to inspect the data as it flows through the pipeline.

```javascript
const result = users
  .filter((user) => user.active)
  // console.log('After active filter:', resultFromActiveFilter)
  .filter((user) => user.score > 85)
  // console.log('After score filter:', resultFromScoreFilter)
  .map((user) => user.name.toUpperCase());
```

### Problem 2: State changes unexpectedly

**Symptoms:** A piece of state (e.g., in a React component or Redux store) changes when it shouldn't have, leading to bugs or infinite re-renders.
**Solution:** This is almost always due to accidental mutation. Audit your code to ensure you are not modifying state directly. Use the spread syntax (`...`) for objects and arrays (`[...arr, newItem]`) to create copies. Libraries like `Immer` can make this process safer and more concise.

## Examples in Context

  - **Frontend Web Development**: The **Elm Architecture** (Model, Update, View) is a purely functional pattern for building web apps. It has heavily influenced libraries like Redux.
  - **Data Analysis**: Running a series of transformations on a large dataset to filter, shape, and aggregate it for a report.
  - **Concurrency**: Handling multiple events or requests simultaneously without running into race conditions, because data is immutable and shared state is avoided.

## References

  - [MDN Web Docs: Functional Programming](https://www.google.com/search?q=https://developer.mozilla.org/en-US/docs/Glossary/Functional_programming)
  - [Professor Frisby's Mostly Adequate Guide to Functional Programming](https://mostly-adequate.gitbook.io/mostly-adequate-guide/)
  - [Ramda Documentation](https://ramdajs.com/docs/)

## Related Topics

  - Immutability
  - Functional Reactive Programming (FRP)
  - The Elm Architecture
  - Algebraic Data Types