Functional JavaScript - KeynesYouDigIt/Knowledge GitHub Wiki
FP vs. OOP
- OOP breaks problems into nouns, FP breaks problems into verbs
- OOP is about subtly modifying the state of objects, while FP is about minimizing observation state changes- minimizing the surface area of change to the smallest area possible
- Functions are used to abstract out behavior
- FP uses first-class functions (functions stored and used as values)
Speed
Functional code is slower than imperative code, all things being equal. All things aren't equal, though. Static optimization can "inline" (copy/paste) the imperative code from a functional implementation.
Predicates to Comparators
- A predicate returns
true
orfalse
- A comparator returns
-1
,0
, or1
- You can use a function to translate a predicate to a comparator
function toComparator(predicate){
return (x, y) => predicate(x, y) ? 1 : -1;
}
function isGreater(x, y){
return x > y;
}
[3, 2, 1].sort(toComparator(isGreater))
Applicatives
"Apply" a function to table-like data
map
,filter
,reduce
all
,any
find
,findWhere
,where
- Find matchesreduceRight
- Reduce, but starts at the end of the collectionreject
- Opposite offilter
sortBy
- Sorts a collection of objects based on a propertygroupBy
- Array to object, keys are the group, values are an array of matchescountBy
pairs
/object
- Split an object into an array of key/value arrays, or combine them back into an objectinvert
- Flips an array (such as to reverse keys and values)omit
/pick
- Removes a key from an object, like a password / Whitelist keys for an objectkeys
,values
,pluck
- Get the keys, values, or all the values for one key in an array of objects- "Collection-centric programming"
Scope
- Lexical scope - Look up the scope from inner to outer
- Dynamic scope - Defined at run-time (
this
), uses a stack - Function scope vs. Block scope
- Free variable - Closed-over variable referenced in an inner-scope
- Shadowing - Overwriting a variable in an inner scope
Higher-Order Functions
Given a collection of people, finds the oldest person:
finder(plucker("age"), Math.max, people);
- For: Use functions for iterating somemthing a specific number of times (
repeatedly
) - While:
function iterateUntil(fn, test, initialValue)
- Static values are often represented with
k
- Combinator: Lambda with no free variables
- Currying: Returning a function from a function
- Use a free variable to have shared state between multiple calls to the same function (incrementor)- this isn't pure and makes a function stateful though
- Referential transparency: Only reliant on input to produce output, no free variables
- Return a function with a bound parameter is a way to "configure" an object
- You can attach metadata to a function as a property, like an error message, and return it in a higher-order function
Functional Composition
Snapping together functions like legos.
- Dispatching: Try each of these functions in order until one of them returns a value
- You can use a chain of functions to replace a switch statement. Each function takes a test function and a function that will be invoked if the test passes. If the function returns undefined, it tries the next function.
- Mutation is a low-level operation - hide it as far down as you can.
const someDataTransformation = dispatch(
isA("notify", notifyUser),
isA("join", joinUser),
alertUser, // default
);
Currying
Create a function that returns other functions, applying the arguments from the first
- Used to create fluent interfaces and build new functions
- Can be done from the right or left
curry1
,curry2
,curry3
- If the number of parameters is arbitrary, use partial application instead
function outside(variable1){
return function inside(variable2){
return variable1 * variable2;
};
}
Partial Application
Currying builds a function that has access to free variables, partial application progressively binds arguments to functions
function outside(variable1){
return inside.bind(null, variable1);
}
function square(number){
return number * number;
}
// `condition` accepts validators, and returns errors if validations fail or applies the bound function otherwise
const squarePostConditions = condition(
validator("Needs to be greater than 0", greaterThan(0)),
);
const squarePreconditions = condition(
validator("Must a number", isNumber),
);
// `partial` calls the first function, and binds the second function as its first argument
const validatedResult = partial(squarePostConditions, identity);
const validatedSquare = partial(squarePreconditions, square);
// Evaluates right to left
const safeSquare = compose(
validatedResult,
validatedSquare,
);
safeSquare(2);
This code:
- Passes 2 to the
safeSquare
function - The 2 is passed into the
validatedSquare
function validatedSquare
applies the preconditions to 2- If there are no errors, it passes 2 to the square function, which was bound as its first argument
- 4 is passed to the
validatedResult
function validatedResult
passes 4 to thesquarePostConditions
function- If there are no errors, it passes 4 to the identity function, which was bound as its first argument
Recursion
Reasons to use recursion:
- One abstraction that's applied to progressively smaller subsets
- Recursion can hide mutable state
- Can process lazily and work with infinitely large lists
How to write a self-recursive function:
- Know when to stop
- Decide how to take one step
- Break the problem into that step and a smaller problem
- Can be used to walk a graph. Can keep memory of visited nodes, passing those in to each call as well
- A tail-recursive call is one that makes the recursive call as its last step. Some languages use this to clean up resources used by intermediate steps- otherwise you'll blow the call stack with large data.
- Conjoin and disjoin: Using recursion to make
and
andor
with short-circuits - Can be used to deep clone
- Codependent functions (mutually recursive) are functions that call each other
- Can be used to flatten arrays
Trampolining is where nested calls are flattened out, thus getting around call-stack limitations. Each call returns a partially applied function. Instead of calling it once, the trampoline calls every successive function until they're done. This transfers the problem from the call stack to the heap, which is much larger.
To work with infinite data, split the data into the head and the tail.