Composing functions
- What is function composition?
→ Function composition is the process of applying a function to the output of another function
- What is object composition?
The problem is that you can’t avoid composition just because you’re not aware of it. You still do it –
We spend more time maintaining software than we do creating it from scratch, and our bugs impact billions of people all over the world.
- When you use pipe() (and its twin, compose()) You don’t need intermediary variables.
- Writing functions without mention of the arguments is called “point-free style”
- Using the pipe form, we eliminated 3 variables, Working memory models typically involve 4–7 discrete quanta
- Concise code also improves the signal-to-noise ratio of your code.
Composing Objects
“Favor object composition over class inheritance”
GoF catalog four kinds of object compositional relationships:
- Delegation (Example: State, Strategy, Visitor), maybe
behave-as
. - Acquaintance (When an object knows about another object by reference, usually passed as a parameter: a
uses-a
relationship) - Aggregation (When child objects form part of a parent object:
has-a
relationship, like DOM children, are component elements in a DOM node) - Class inheritance can be used to construct composite objects(
is-a
relationship) but it’s a restrictive and brittle way to do it (the rigid, tightly-coupled approach of class inheritance.)
They’re encouraging you to favor has-a and uses-a relationships over is-a relationships.
The main pitfall about is-a class relationships (Class inheritance):
- The tight coupling problem: because the child classes are dependent on the implementation of the parent class, class inheritance is the tightest coupling available in object-oriented design.
- The fragile base class problem: Due to tight coupling, changes to the base class can potentially break a large number of descendant classes. Potentially in code managed by third parties. The author could break code they’re not aware of.
- The inflexible hierarchy problem: with single ancestor taxonomies, given enough time and evolution, all classes taxonomies are eventually wrong for new use-cases.
- The duplication by necessity problem: Due to inflexible hierarchies, new use cases are often implemented by duplication, rather than an extension.
- The gorilla/banana problem: The problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them (You wanted a banana but what you got were a gorilla holding the banana and the entire jungle.” ∼ Joe Armstrong)
The most common form of object composition in JavaScript is known as object concatenation (aka, concatenative inheritance: informally, “mixin composition”).
What we won’t do is say that functional programming is better than object-oriented programming, or that you must choose one over the other. OOP vs FP is a false dichotomy.
You want to select the simplest, most flexible solution for the task at hand. No matter how you write software, you should compose it well.
The essence of software development is composition.
The DAO of Immutability:
Immutability: The true constant is change. Mutation hides change. Hidden change manifests chaos. Therefore, the wise embrace history.
If you have a dollar, and I give you another dollar, it does not change the fact that a moment ago you only had one dollar, and now you have two. Mutation attempts to erase history, but history cannot be truly erased. When the past is not preserved, it will come back to haunt you, manifesting as bugs in the program.
Separation: Logic is thought. Effects are action. Therefore the wise think before acting, and act only when the thinking is done.
If you try to perform effects and logic at the same time, you may create hidden side effects which cause bugs in the logic. Keep functions small. Do one thing at a time, and do it well.
Composition: All things in nature work in harmony. A tree cannot grow without water. A bird cannot fly without air. Therefore the wise mix ingredients together to make their soup taste better.
Write functions whose outputs will naturally work as inputs to many other functions. Keep function signatures as simple as possible. (Imagine use of Pipe or Compose)
Conservation: Time is precious, and effort takes time. Therefore the wise reuse their tools as much as they can. The foolish create special tools for each new creation.
Type-specific functions can’t be reused for data of a different type. Wise programmers lift functions to work on many data types, or wrap data to make it look like what the function is expecting. Lists and items make great universal abstractions.
Flow: still waters are unsafe to drink. Food left alone will rot. The wise prosper because they let things flow.
A list expressed over time is a stream.
Shared objects and data fixtures (Mutation/Side-effects) can cause functions to interfere with each other. Threads competing for the same resources can trip each other up. A program can be reasoned about and outcomes predicted only when data flows freely through pure functions.
Note: An example of that is when your UT are consuming fixture data which is changed by each UT, we will lose predictability)
The Rise and Fall and Rise of Functional Programming
Alonzo Church, and Alan Turing. They produced two different, but equivalent universal models of computation.
In lambda calculus, functions are king. Everything is a function, including numbers. Thinking of functions as the atomic building blocks.
There are three important points that make lambda calculus special:
- Functions are always anonymous.
- Functions in lambda calculus are always unary, meaning they only accept a single parameter (In case that need a function with more than one argument use curry).
- Functions are first-class
Together, these features form a simple, yet expressive vocabulary for composing software using functions as the primary building block.
The Fall of Functional Programming
In 1972, Alan Kay’s Smalltalk was formalized, and the idea of objects as the atomic unit of composition took hold. Smalltalk’s great idea about component encapsulation and message passing…
but
It was distorted in the 80s and 90s by C++ and Java into a horrible idea about inheritance hierarchies and is-a relationship for feature reuse.
In the next 30 years (1990–2010), for most of us, creating software was a bit of a nightmare for 30 years. Dark times. ;)
The Rise of Functional Programming
By 2015, the idea of functional programming was popular again. To make it simpler, the JavaScript specification got its first major upgrade of the decade and added the arrow function. Arrow functions were like rocket fuel for the functional programming explosion in JavaScript. Today it’s rare to see a large application that doesn’t use a lot of functional programming techniques.
Some keywords
• Pure functions (Referential transparency, etc)
• Function composition
• Avoid shared state
• Avoid mutating state
• Avoid side effects
• Declarative vs imperative
Referential transparency
In algebra, this means exactly the same thing as writing 4, so any place you see f(2) you can substitute 4. This property is called referential transparency (If you want referential transparency, you need to use pure functions).
KISS
Perhaps the most important design principle in computer science is KISS (Keep It Simple, Stupid). I prefer “Keep It Stupid Simple”. Pure functions are stupid-simple in the best possible way.
Shared state
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.
- There are many sources of concurrency in JavaScript. API I/O, event listeners, web workers, iframes, and timeouts can all introduce non-determinism into your program. Combine that with a shared state, and you’ve got a recipe for bugs.
- A race condition is also a common problem associated with a shared state.
- When you avoid a shared state, the timing and order of function calls don’t change the result of calling the function.
- Remove function call timing dependency, and you eliminate an entire class of potential bugs.
- Functional programming tends to reuse a common set of functional utilities to process data. Object-oriented programming tends to colocate methods and data in objects. In most object-oriented software, those colocated methods can only operate on the type of data they were designed to operate on, and often only the data contained in that specific object instance.
Functional programming avoids shared state: “Non-determinism = parallel processing + mutable state”
Functional programming is usually more declarative than imperative, meaning that we express what to do rather than how to do it.
Other examples of programming paradigms include object oriented programming, where data and behaviors are colocated
Containers, Functors, Lists, and Streams
Functor:
A functor is a data structure that can be mapped over. So you need to think in a container that has an interface that can be used to apply a function to the values inside it.
Stream:
“A list expressed over time is a stream.”
Conclusion:
- Pure function over the shared state and side effects
- Immutability over mutable data
- Function composition over imperative flow control
- Generic Utilities that act on many data types over object methods that only operate on their colocated data.
- Declarative vs Imperative (what to do, rather than how to do it)
- Expressions over statements
Using default assignments wherever it makes sense can help you write more self-documenting code.
Function composition using point-free style creates very concise & readable code
Data-last: The last thing the function expects is the data. Since it’s curried this allows you to define what the function will do, assign it to a variable, then later give it the data in a composed function (or on its own).
More detail: https://hackernoon.com/function-composition-with-lodash-d30eb50153d1
NOTE: Nice article about point-free style programming https://lucasmreis.github.io/blog/pointfree-javascript/
Abstraction is simplification
The process of decomposition is the process of abstraction. Successful abstraction implies that the result is a set of independently useful and recomposable components.
Software solutions should be decomposable into their component parts, and recomposable into new solutions, without changing the internal component implementation details.
Generalization
Is the process of finding similarities (the obvious) in repeated patterns and hiding the similarities behind an abstraction.
Specialization
Is the process of using the abstraction, supplying only what it is different (the meaningful) for each use case.
Abstraction in software takes many forms:
- Algorithms
- Data structures
- Modules
- Classes
- Frameworks
- a Function
“Sometimes, the elegant implementation is a function. Not a method. Not a class. Not a framework, just a function”
Functions make a great abstraction because they possess the property that are essential for a good abstraction:
- Identity: The ability to assign a name to it and reuse it in different contexts.
- Composable: The ability to compose simple functions to form more complex functions.
The key characteristic of a good abstraction:
- Composable
- Reusable
- Independent
- Concise
- Simple