Functional programming - a paradigm for structuring our complex code
Functional programming is a programming paradigm that treats computation as the evaluation of mathematical functions and avoids changing state and mutable data. In functional programming, the focus is on using pure functions, immutability, and higher-order functions to build programs. This approach aims to write code in a declarative and concise manner, emphasizing the “what” is being done rather than the “how” it’s done.
Imagine if we could structure our code into individual pieces where almost every single line is self-contained
- [/] The only thing any given line depends on are inputs (explicitly stated in that very line)
- [/] The only consequences of the line would be its output (explicitly stated in that very line)
- [/] And each line could get a nice human-readable label for when we use it!
Characteristics of functional programming
- [/] Pure Functions:
- ➡️ Pure functions are the cornerstone of functional programming. They always produce the same output for the same input and have no side effects. This means they don’t modify external state and don’t rely on external state changes. Pure functions are deterministic and easy to reason about.
- [/] Immutability:
- functional programming, data is immutable, meaning it cannot be changed once created. Instead of modifying data in place, new instances are created. Immutability eliminates many common bugs related to data modification and simplifies the reasoning process.
- [/] First-Class and Higher-Order Functions:
- ➡️ Functional in functional programming are first-class citizens, meaning they can be treated like any other value. They can be passed as arguments to other functions, returned from functions, and assigned to variables. Higher-order functions are functions that take other functions as arguments or return functions as results. They enable abstraction and composition.
- [/] Functional Composition:
- ➡️ Functional programming encourage composing functions together to create more complex functions. This is achieved by chaining or combining functions to perform multiple operations in a sequence. This approach promotes code reusability and readability.
- [/] Avoiding Mutability and Loops:
- ➡️ Functional programming discourages the use of mutable data and traditional loop constructs. Instead, recursion is often used to solve problems. This leads to code that is more elegant, though it may require a different way of thinking about problem-solving.
- [/] Lazy Evaluation:
- ➡️ In functional programming languages, evaluation of expressions is delayed until their results are actually needed. This can lead to more efficient use of resources and enable the handling of infinite data structures.
- [/] Referential Transparency:
- ➡️ An expression is referentially transparent if it can be replaced with its value without changing the program’s behavior. This property allows for easy reasoning about code and simplifies testing and optimization.
- [/] Closures:
- ➡️ Closures allow functions to capture and remember the lexical scope in which they were created. They enable powerful encapsulation and the creation of data privacy.
- [/] Immutable Data Structures:
- ➡️ Functional programming often provides specialized data structures like persistent data structures, which allow modification operations to create new versions of the data structure instead of modifying the existing one.
Functional programming languages like Haskell, Lisp, and Erlang are designed with these principles in mind. However, functional programming concepts can also be applied to languages like JavaScript, Python, and Java through libraries and specific programming patterns.
JavaScript the Hard Parts - Functional Programming
- [>] Principles of JavaScript
- [>] Higher order functions
- [>] Arrow and anonymous functions
- [>] reduce, filter and chaining higher order functions
- [>] Function composition & pure functions
- [>] Closure
- [>] Function decoration
- [>] Partial application and currying
Principles of javascript
JavaScript Execution.
What happens when JavaScript executes (runs) my code?
Code is saved (defined) in functions - to be run later
As soon as we start running our code, we create a global execution context
Thread of execution (parsing and executing the code line after line)
Live memory of variables with data (known as a Global Variable Environment)
Running / calling / invoking a function
Running a function is not the same as defining a function
When you execute a function you create a new execution context comprising:
The thread of execution (we go through the code in the function line by line)
A local memory (' Variable environment') where anything defined in the function is stored
The call stack
- We keep track of the functions being called in JavaScript with a Call stack
- Tracks which execution context we are in - that is, what function is currently being run and where to return to after an execution context is popped off the stack One global execution context, multiple function contexts
Higher order functions
what are they?
Higher-order functions are functions that can accept other functions as arguments or return functions as results. They allow for code abstraction and composition.
An example of a higher order function.
Why would we need a higher order function ?
Let's break the dry principle to understand higherOrderFunction.
The Bad way of doing things
Suppose we have a function copyArrayAndMultiplyBy2. Let’s diagram it out
What if you want to copy array and divide by 2?
well it would loop something like this.
What if we wand to add three ?
What principle are we breaking ? the DRY principle. Do not repeat ourselves.
The Great way of doing things using high order functions.
We could generalize our function so that we pass in our specific instruction only when we run the copyArrayAndManipulate function!
How is this possible ?
Functions in javascript = first class objects
Higher-order functions are made possible by the concept of functions being first-class citizens in a programming language. When a programming language treats functions as first-class citizens, it means that functions can be used in the same way as other data types, such as integers, strings, or objects. This enables several features that are essential for higher-order functions:
They can co-exist with and can be treated like any other javascript object
- [/] Assigned to variables and properties of other objects
- [/] Passed as arguments into functions
- [/] Returned as values from functions
Higher order functions
- [/] Easier to add features - we don’t need to build a brand new copyArrayAndAdd3 function - just use copyArrayManipulate with the input of add3. Higher order functions keep our code DRY
- [/] More readable - copyArrayManipulate(multiplyBy2) - I know what this is doing more readily than the for loop style
- [/] Easier to debug - As long as we understand what’s happening under-the-hood
Arrow and anonymous functions
Arrow functions
Arrow functions are a concise way to define functions in JavaScript. They were introduced in ECMAScript 6 (ES6) and are particularly useful for writing shorter function expressions. Arrow functions have a slightly different syntax compared to traditional function expressions.
So where the function is a single expression to evaluate and then return, ES2015 lets us remove the {} and return keyword
We can even remove the parenthesis if there’s only 1 parameter (expected input)
Key features of arrow functions:
- [/] No Binding of
this
:- ➡️ Arrow functions do not bind their own
this
value. Instead, they inherit thethis
value from the enclosing lexical scope (the context in which they are defined). This behavior can be beneficial in some cases, but it might lead to unexpected behavior if not understood properly.
- ➡️ Arrow functions do not bind their own
- [/] Concise Syntax:
- ➡️ If the function body contains only a single expression, you can omit the curly braces
{}
and thereturn
keyword. The result of the expression will be automatically returned.
- ➡️ If the function body contains only a single expression, you can omit the curly braces
- [/] No Named Arrow Functions:
- ➡️ Arrow functions are always anonymous; they don’t have a name. They are usually assigned to variables or used directly as arguments to other functions.
Anonymous Functions:
An anonymous function is a function that does not have a name. It’s defined without a specific identifier. Anonymous functions can be created using both traditional function expressions and arrow functions.
Example of an anonymous function using an arrow function:
Map, Reduce, Filter and chaining higher order functions
Reduce
The reduce()
function in JavaScript is a powerful higher-order function that iterates over an array and accumulates a single value based on the elements of the array. It’s incredibly versatile and can be used to solve a wide range of problems. To fully understand reduce()
and its intricacies, let’s dive deep into its usage and concepts.
#### reduce and reducers
- [p] The most versatile higher order function of all
- [p] Takes a mental shift to look at problems through the reduce lens
- [p] Can even enable function composition (to come)
Syntax
- 🔥
array
: The array you want to perform the reduction on. - 🔥
callback
: A function that is called on each element of the array. It takes four arguments: the accumulator, the current value, the current index, and the array itself. - 🔥
initialValue
: An optional initial value for the accumulator. If not provided, the first element of the array is used as the initial value and the callback starts from the second element.
Key concepts and intricacies of reduce()
- Accumulator: The accumulator is a value that is updated on each iteration of the callback function. It accumulates the result as the callback processes each element of the array.
- Callback Function: The callback function provided to
reduce()
defines how the reduction is performed. It takes the accumulator and the current value as arguments and returns the updated accumulator value. - Order of Execution: The callback function is called for each element of the array in order. It starts with the first element (or the initial value if provided) and processes each subsequent element.
- Return Value: The final result of the reduction is the last value of the accumulator after all iterations are complete.
- Chaining:
reduce()
can be chained with other array methods likemap()
,filter()
, andforEach()
to perform complex transformations and computations. - Use Cases:
reduce()
can be used for a wide variety of tasks, such as summing up array elements, finding the maximum or minimum value, flattening arrays, counting occurrences, and more.
Example of summing up array elements using
reduce()
:
How else could we ‘combine and use, combine and use’?
- ➡️ Take the number 0 and combine with array[0] by adding, take that combined value and combine with array[1] by adding and so forth…
- ➡️ Take the empty string “ ” and combine with array[0] by appending, take that combined value and combine with array[1] by appending and so forth…
We’d want to write our function so that it could handle:
- 🔥 Any ‘accumulator’ (array, string, number)
- 🔥 Any combining logic/code/functionality (the ‘reducer’)
And we can ‘chain’ these higher order functions - pass the output of one as the input of the next
The output of each higher order function (HOF),where it’s an array, has access to all the HOFs (map, filter, reduce) through the prototype chain
Function composition & pure functions
Function composition is a fundamental concept in functional programming where you combine two or more functions to create a new function. The output of one function becomes the input of another, allowing you to build more complex and reusable operations from simpler functions
- ➡️ Chaining with dots relies on JavaScript prototype feature
- ➡️ functions return arrays which have access to all the HOFs (map, filter, reduce)
- ➡️ I’m passing my output into the next function automatically
What if I want to chain functions that just return a regular output
e.g. multiplyBy2, add3, divideBy5
Or we can use the fact that JavaScript evaluates every function call before it moves on
(Btw This relies on our functions being ‘referentially transparent’ - we can replace the call to the function with its return value with no consequences on our app)
We’re combining a function with a value to get a result then combining that result with another function to get another result and so on
And this really makes us have what we desire the most in our function programming paradigm
The Advantages of Function composition
- [p] Easier to add features
- ➡️ This is the essential aspect of functional JavaScript - being able to list of our units of code by name and have them run one by one as independent, self-contained pieces
- [p] More readable
- ➡️ reduce here is often wrapped in compose to say ‘combine up’ the functions to run our data through them one by one. The style is ‘point free’
- [p] Easier to debug
- ➡️ I know exactly the line of code my bug is in - it’s got a label!
Pure Functions
Pure functions are the cornerstone of functional programming. They always produce the same output for the same input and have no side effects. This means they don’t modify external state and don’t rely on external state changes. Pure functions are deterministic and easy to reason about.
- 🔥 Functions as tiny units to be combined and run automatically must be highly predictable
- 🔥 We rely on using their evaluated result to pass the input to the next unit of code (automatically). Any ‘side effects’ would destroy this
Pure functions should
- 🔥 No random values
- 🔥 No current date/time
- 🔥 No global state(dom,file,db,etc)
- 🔥 No mutation of parameters
Benefits of pure function
- 💡 Self documenting
- 💡 Easily testable
- 💡 Concurrency
- 💡 Cacheable
Pure function's only consequence is their returned value.
This is a perfect example of a
non pure function
Immutability
If we want the only consequence of map to be on that line and to achieve ‘referential transparency’ ( I can replace the function call with its output and it’s the same) - then I need to preserve my data and not manipulate it
JavaScript passes a reference (‘link back’) to the array when it’s inserted into the function map. If we change (‘mutate’) the input array our function is not pure it’s unpredictable I can’t figure out what it does just be reading it and looking at its output there in that line - undoes all our hard work
Pure functions & immutability
- ➡️ Easier to add features - Every saved function be safely used in new combinations , confident it won’t break other parts of the app
- ➡️ More readable - Every line is ‘complete’ - it’s fully descriptive - exactly what it does is discoverable its name and limited to that input/output
- ➡️ Easier to debug - No 1000s of lines of interdependence
More on function composition
Basic Function Composition:
You can compose two functions f and g to create a new function that applies g to the result of f. This is often denoted as (g ∘ f)(x) or g(f(x)).
Pipe Function Composition:
In pipe composition, you apply a sequence of functions from left to right. Each function takes the result of the previous one as input.
Compose and Pipe in Lodash:
The Lodash library provides utility functions _.compose
and _.flow
(equivalent to pipe) for function composition.
Functional Programming Libraries:
Libraries like Ramda and Lodash/fp provide extensive support for function composition and currying, making it easier to work with functional programming concepts.
Currying:
Currying is the process of converting a function that takes multiple arguments into a sequence of functions, each taking a single argument. Currying is often used in function composition to create partially applied functions.
Closure
- ➡️ Most esoteric concept in JavaScript
- ➡️ Functions are our units to build with but they’re limited - they forget everything each time they finish running - with no global state
- ➡️ Imagine if we could give our functions memories
Reminding ourselves of how functions actually work
No memory of the previous execution - imagine if we could give our functions permanent memories It begins with returning a function from a function
Closure is when a function remembers its lexical scope even when the function is executed outside that lexical scope
What are we closing over ?
one of the biggest perpetual frustration is the misbelief that when we close over our variables, we are preserving the values
Don't think of closure as capturing values, rather
preserving access to variables
The bond
When a function is defined, it gets a bond to the surrounding Local Memory (“Variable Environment”) in which it has been defined
The ‘Backpack’
- [f] When incrementCounter is defined, it gets a bond to the surrounding Local Memory of live data in outer in which it has been defined
- [f] We then return incrementCounter out of outer into global and store it in myNewFunction
- [f] BUT we maintain the bond to the surrounding live local memory from inside of outer
- ➡️ this live memory gets ‘returned out’ attached to the incrementCounter function definition and is therefore now stored attached to myNewFunction - even though outer’s execution context is long gone
- [f] When we run myNewFunction in the global execution context, it will first look in its own local memory for any data it needs (as we’d expect), but then in its ‘backpack’ before it looks in global memory
What’s the official name for the ‘backpack’?
COVE
The Closed over Variable Environment (COVE) or ‘Closure’ This ‘backpack’ of live data that gets returned out with incrementCounter is known as the ‘closure’
P.L.S.R.D:
P.L.S.R.D: Persistent Lexical Scope Referenced Data
#### The ‘backpack’ (or ‘closure’) of live data is attached incrementCounter (then to myNewFunction) through a hidden property known as scope which persists when the inner function is returned out
Closure in functional JavaScript
- 🔥 Easier to add features - ➡️ Our functions can now have persistent permanent memories attached to them - it’s going to let us build dramatically more powerful functions
- 🔥 Easier to debug
- ➡️ Definitely need to know how it’s working under the hood
Function Decoration
Function decoration, also known as function decorating or function decorators, is a design pattern in programming where a function is extended or enhanced with additional functionality by wrapping it with another function
-
🔥 Base Function:
- ➡️ You have a base function that performs a specific task or operation.
-
🔥 Decorator Function:
- ➡️ You create a decorator function that takes the base function as an argument and returns a new function.
-
🔥 Enhanced Function
- ➡️ The new function returned by the decorator wraps or extends the behavior of the base function. It can add pre-processing or post-processing steps, modify the input or output, or perform any other desired actions.
-
[p] Function decoration
- ➡️ Easier to add features - We can ’pseudo’ edit our functions that we’ve already made - into functions that behave similar but with bonus features!
- ➡️ Easier to debug - Definitely need to know how it’s working under the hood!
Partial Application
Function composition is powerful but every function needs to behave the same way
- Taking in one input and returning out one output
- What if I have a function I want to use that expects two inputs This is ‘arity mismatch’
- We need to ‘decorate’ our function to prefill one of its inputs This means creating a new function that calls our multiagent function - with the argument and the multi-argument function stored conveniently in the backpack
It’s known as ‘Partial application’
- 🔥 Partial application and currying
- 💡 In practice we may have to prefill one, two… multiple arguments at different times
- 💡 We can convert (‘decorate’) any function to a function that will accept arguments one by one and only run the function in full once it has all the arguments
- 💡 This is a more general version of partial application
Partial application & currying
- 🔖 Easier to add features
- ➡️ Mismatched arity - no problem! We write a function multiply once and then reuse it for different situations by ‘editing’ its arguments
- 🔖 More readable
- ➡️ We can use our composition/reduce to list out functions to run one-by-one on our data, even if the functions excepted more than 1 input!
- 🔖 Easier to debug
- ➡️ Individual units of functionality possible even with 1+ input expected
Tips and tricks
Stack overflow
A "stack overflow" refers to a specific type of error that occurs in computer programming when the call stack, a data structure used by the program to manage function calls and execution, becomes too full or exceeds its allocated memory space. This error typically occurs when a program recurses (calls a function within itself) without a proper termination condition, causing the call stack to accumulate too many function calls and run out of space.
Referential transparency
Referential transparency is a concept in functional programming and computer science that refers to the property of a function or expression, where it can be replaced with its corresponding value (result) without changing the program's behavior. In other words, if a function is referentially transparent, you can substitute its call with the result it produces, and the program will still behave the same way.
As a simple example, we can use the expression
2 + 3
. This expression is pure (i.e. no side effects), so it is referentially transparent. We can replace2 + 3
with its result5
without changing the behavior of the program. For example, if the program was computing the following expression:((2 + 3) + 2) / 7
, then computing((5) + 2) / 7
would yield the same result,1
. The behavior of the program is preserved, although its definition has been simplified.
Heap
Heap: a much larger part of the memory that stores everything allocated dynamically, that allows a faster code execution, and protects it from corruption and makes the execution faster.
Scope
In the context of programming, a scope refers to the region or context in which a variable, function, or identifier is defined and can be accessed. Scopes determine where in your code a particular variable or function is visible and can be used.
Arity
Arity: is a term used in computer science and mathematics to describe the number of arguments or parameters that a function or operation can accept. It's essentially a measure of the function's parameter count.