- Original text by Kyle Simpson
- Below are my personal learning notes, or highlights I want to remember from the text along with my learning notes
Kyle Simpson's full text available at: https://github.com/getify/Functional-Light-JS
- "teach it later" test
After I've written a piece of code, I leave it alone for a few hours or days, then come back and try to read it with fresh eyes, and pretend as if I need to teach or explain it to someone else. Usually, it's jumbled and confusing the first few passes, so I tweak it and repeat!
- FP asks that we treat functions the way they work in math
- Nice example of
f(x) = 2x + 10
as a function to map x-y coordinates - That function will always return a value
- Nice example of
A procedure is an arbitrary collection of functionality. It may have inputs, it may not. It may have an output (return value), it may not. A function takes input(s) and definitely always has a
return
value.
- Super interesting distinction
- To leverage FP, plan to avoid procedures wherever possible
- Aim to write functions that take inputs and return outputs
function foo(x,y){..}
foo(3, 10)
- Arguments: values you pass in
3, 10
- Parameters: named variables inside the function that receive those passed-in values
x, y
function foo(x,y,z) {..}
foo.length; // 3
- Can check a function's arity at runtime with
length
- Use case: determine where a function reference is coming from if there are multiple sources w/different argument numbers
// `fn` is set to some function reference
// `x` exists with some value
if (fn.length == 1) {
fn( x );
}
else if (fn.length == 2) {
fn( undefined, x );
}
else if (fn.length == 3) {
fn( undefined, undefined, x );
}
For example, imagine a case where an
fn
function reference could expect one, two, or three arguments, but you always want to just pass a variablex
in the last position
function foo(x,y = 2) { .. }
function bar(x,...args) { .. }
foo.length; // 1
bar.length; // 1
- But, might not behave as expected!
- So more advisable to use
...args
- So more advisable to use
function foo(x,y,z,...args) {
console.log( x, y, z, args );
}
foo(); // undefined undefined undefined []
foo( 1, 2, 3 ); // 1 2 3 []
foo( 1, 2, 3, 4 ); // 1 2 3 [ 4 ]
foo( 1, 2, 3, 4, 5 ); // 1 2 3 [ 4, 5 ]
- Simpson likes to say
...
gathers in an assignment position (e.g. a parameters list)- This is sometimes also referred to as rest, for "all the rest"
- I've used as rest to destructure some props/parameters in a React component into aliased variables and capture all the remaining ones
function foo(...args) {
// ..
}
args
here would be an array with all passed argumentsargs.length
will be an accurate count of how many arguments are passed in
function foo(...args) {
console.log( args[3] );
}
var arr = [ 1, 2, 3, 4, 5 ];
foo( ...arr ); // 4
- Nice simple demo how
...
behaves symmetrically- In the function declaration,
foo(...arr)
is gathering all parameters into a single array (assignment position) - In the call,
foo(...arr)
is spreading thearr
array to individually pass in each value as an argument (value-list position)
- In the function declaration,
function foo( [x,y,...args] = [] ) {
// ..
}
foo( [1,2,3] );
- Model of naming the first elements in an array and gathering all the rest as
args
- Left-side
[]
array brackets in parameter parenthesis indicates destructuring
- Left-side
function foo(params) {
var x = params[0];
var y = params[1];
var args = params.slice( 2 );
// ..
}
- The same operation as in the previous code sample but in an imperative style
- Key benefit of declarative style is focusing on the output or desired end state
- In contrast, in this imperative code we need to mentally execute each step to understand the outcome
- The destructuring and
gather
operator allow readers of our code to focus on the outcome rather than how the outcome is arrived at - It hides some of the details, which results in more focused, readable code
- The destructuring and
Wherever possible, and to whatever degrees our language and our libraries/frameworks will let us, we should be striving for declarative, self-explanatory code.
function foo( {x,y} = {} ) {
console.log( x, y );
}
foo( {
y: 3
} );
// undefined 3
- At the
foo
call-site we don't need to worry aboutx
- i.e. We don't need to pass in
x: undefined
to match the destructuring order of the parameters - Parameter object destructuring is a powerful JS tool here
- Some other languages have named arguments that allow you to label arguments with the parameter they should map to at the call-site (not JS!)
- i.e. We don't need to pass in
- In FP, a function that only takes in a single argument is easier to compose with another function's single output
- Also a point on unordered parameters
- Order of the destructuring or object argument keys doesn't matter
Named arguments are much more flexible, and attractive from a readability perspective, especially when the function in question can take three, four, or more inputs.
- Pretty sure this is why we can destructure props in a React component without worrying about the order
- The
props
object will have keys and any key can be accessed via object destructuring regardless of order
- The
- In FP, our functions should have an output
- Functions should explicitly return a value
- If not otherwise stated, all JS functions will just return
undefined
, but that default output is more aligned with JS that conducts procedures, which we're trying to avoid - To return multiple values, we can use an object or an array
- Consider if there needs to be multiple returned objects though, or if small, simple unary functions would be an option
Collecting multiple values into an array (or object) to return, and subsequently destructuring those values back into distinct assignments, is a way to transparently express multiple outputs for a function.
- Caution to avoid multiple
return
s that impose an implicit logic to a function- Suggestion to be as explicit as possible
- Code sample where multiple
return
s make tracing the function's output tricky - Refactored code is more verbose, but has clear, explicit flow control via
if
statements
- Point that
return
as a means of flow control isn't always clear
- Higher order functions are functions that treat other functions as values
function formatter(formatFn) {
return function inner(str){
return formatFn( str );
};
}
var lower = formatter( function formatting(v){
return v.toLowerCase();
} );
var upperFirst = formatter( function formatting(v){
return v[0].toUpperCase() + v.substr( 1 ).toLowerCase();
} );
lower( "WOW" ); // wow
upperFirst( "hello" ); // Hello
- Various examples of closure in this section, but this one is nice
- This would be simpler without the
formatter
higher-order function, but this example helps me see how the pattern could be useful given more involved logic
- This would be simpler without the
- Three arguments for naming functions
function foo(fn) {
console.log( fn.name );
}
var x = function(){};
foo( x ); // x
foo( function(){} ); //
foo(() => {}); //
1: Stack-trace debugging
- Anonymous function expressions don't receive any name inference when passed as function arguments
- This is a common use case, and the lack of a function name means stack traces will only identify this as a "anonymous function"
[1, 2].map(v => v.toUpperCase())
/*
TypeError: v.toUpperCase is not a function
at map.v (...)
at Array.map (<anonymous>)
*/
[1, 2].map(function mapper(v) {
v.toUpperCase()
})
/*
TypeError: v.toUpperCase is not a function
at mapper (...)
at Array.map (<anonymous>)
*/
const people = [{nickNameyNames:['foo']}]
people.map( person => person.nicknames[0] || person.firstName );
/*
"TypeError: Cannot read properties of undefined (reading '0')
at your-file.js:3:39
at Array.map (<anonymous>)
at null.js:3:8
at https://static.jsbin.com/js/prod/runner-4.1.8.min.js:1:13924
at https://static.jsbin.com/js/prod/runner-4.1.8.min.js:1:10866"
*/
people.map( function getPreferredName(person) {
return person.nicknames[0] || person.firstName
});
/*
"TypeError: Cannot read properties of undefined (reading '0')
at getPreferredName (your-file.js:6:26)
at Array.map (<anonymous>)
at null.js:3:8
at https://static.jsbin.com/js/prod/runner-4.1.8.min.js:1:13924
at https://static.jsbin.com/js/prod/runner-4.1.8.min.js:1:10866"
*/
2: Reliable self-reference
- Besides debugging, giving a function a syntactic or lexical name is also useful for internal self-reference
- Examples with recursion and event binding
3: Readability
people.map( function getPreferredName(person){
return person.nicknames[0] || person.firstName;
} )
- Could easily imagine this being an anonymous arrow function, but Simpson's point is that the name gives a sense of what the mapping function does that will be a helpful clue for future devs
(function IIFE(){
// You already knew I was an IIFE!
})();
You virtually never see IIFEs using names for their function expressions, but they should.
- While it's always a bit of work to find a good name for a function, if we don't we're "trading ease-of-writing for pain-of-reading"
- Good to avoid
this
in FP because it operates within functions as an implicit parameter- Nice comparison code below
- Prefer explicit inputs
- In particular this helps when wiring multiple functions together, which is very hard if the functions rely on
this
function sum() {
return this.x + this.y;
}
var context = {
x: 1,
y: 2
};
sum.call( context ); // 3
context.sum = sum;
context.sum(); // 3
var s = sum.bind( context );
s(); // 3
- Implicit parameter
function sum(ctx) {
return ctx.x + ctx.y;
}
var context = {
x: 1,
y: 2
};
sum( context );
- Explicit parameter
- Great summary, worthing clicking over to for recap
- Functions aren't just a set of things that happen (procedures)
- Functions should have explicit input and output
- Functions within functions can hold a memory of values from their outer scope (closure)
This is one of the most important concepts in all of programming, and a fundamental foundation of FP.
- Ask if anonymous functions are actually beneficial
- Don't use
this
in FP
function unary(fn) {
return function onlyOneArg(arg){
return fn( arg );
};
}
- Interesting simple utility function that will ignore all but the first argument
- Good example also where "many FPers" would prefer arrow syntax
- Argument that the terseness is at the cost of readability + clear closure boundaries (via brackets)
["1","2","3"].map( parseInt );
// [1,NaN,NaN]
["1","2","3"].map( unary( parseInt ) );
// [1,2,3]
- Nice model of why
unary
could be useful- In the first, erroneous case,
map
is providing the index as a second (radix) argument toparseInt
- The
unary
utility helps to avoid this by ensuring only the first argument (the array value) is passed toparseInt
- In the first, erroneous case,
function identity(v) {
return v;
}
// or the ES6 => arrow form
var identity =
v =>
v;
- Key idea is that functions that seem so simple they could be useless can in fact be useful in FP
var words = " Now is the time for all... ".split( /\s|\b/ );
words;
// ["","Now","is","the","time","for","all","...",""]
words.filter( identity );
// ["Now","is","the","time","for","all","..."]
- This is using coercion to make the empty stings return
false
and hence be filtered out - Side note: good little regex example:
\s
= white space\b
= word boundary
- A function like
identity
might be also be used as a default parameter value in case a function isn't passed in- Pretty sure we've got plenty of this in work code base
- Also useful as default function for
map
orreduce
function constant(v) {
return function value(){
return v;
};
}
// or the ES6 => form
var constant =
v =>
() =>
v;
- Useful utility when an API requires a function as a parameter
- Good example:
.then()
with JS promises
- Good example:
p1.then( foo ).then( constant( p2 ) ).then( bar );
- A warning against simply using an anonymous arrow function to return a value in the second
then
The arrow function [would be] returning a value from outside of itself, which is a bit worse from the FP perspective
- If you can't change a function's signature, utilities can either gather or spread arguments as needed
function foo(x,y) {
console.log( x + y );
}
function bar(fn) {
fn( [ 3, 9 ] );
}
bar( foo ); // fails
foo
expects two parameters in its signature, but the[3, 9]
argument will only providex
function spreadArgs(fn) {
return function spreadFn(argsArr){
return fn( ...argsArr );
};
}
bar( spreadArgs( foo ) ); // 12
- Takes me a minute to wrap my head around this
spreadFn
intercepts the[3, 9]
argument frombar
(helpfully namedargsArr
)- Then it spreads to into its
fn
parameter (herefoo
) - The sequencing of what happens when feels quite zig-zag
- But I can see how this could be a powerful utility
- In Ramda
spreadArgs
is calledapply
function gatherArgs(fn) {
return function gatheredFn(...argsArr){
return fn( argsArr );
};
}
- You've got a function you want to run,
fn
gatherArgs
takes that function and says, "Cool, I'll run that function..."- "BUT I'm going to modify the arguments it receives before running it"
function combineFirstTwo([ v1, v2 ]) {
return v1 + v2;
}
[1,2,3,4,5].reduce( gatherArgs( combineFirstTwo ) );
// 15
- Nice use case for
gatherArgs
reduce()
will always provide two arguments (acc, cur
), andgatherArgs
allows us to use a single argument callback
function partial(fn,...presetArgs) {
return function partiallyApplied(...laterArgs){
return fn( ...presetArgs, ...laterArgs );
};
}
- Super interesting and elegant
- Partial application utility that holds onto a core function in closure and any number of arguments initially passed in
- Then returns a function that will accepts any subsequent arguments, combine them with the initial ones and call the core function
var getPerson = partial( ajax, "http://some.api/person" );
- Straightforward enough, we've added an endpoint URL and
getPerson
will executeajax
with that initial argument and any subsequent ones
// version 1
var getCurrentUser = partial(
ajax,
"http://some.api/person",
{ user: CURRENT_USER_ID }
);
// version 2
var getCurrentUser = partial( getPerson, { user: CURRENT_USER_ID } );
- Pretty interesting, version 2 adds to an already partially applied function
- By reusing something already defined, version 2 is more of an FP approach
var getCurrentUser = function outerPartiallyApplied(...outerLaterArgs){
var getPerson = function innerPartiallyApplied(...innerLaterArgs){
return ajax( "http://some.api/person", ...innerLaterArgs );
};
return getPerson( { user: CURRENT_USER_ID }, ...outerLaterArgs );
}
- Unpacked version 2 looks a bit like the above
- Think
...innerLaterArgs
is{ user: CURRENT_USER_ID }
- Think
- Simpson notes that the extra layer of wrapping may seem weird and wrong, but it's the kind of thing we'll want to get comfortable with in FP
function add(x,y) {
return x + y;
}
[1,2,3,4,5].map( partial( add, 3 ) );
// [4,5,6,7,8]
- The
partial
utility here allows us to make use of the value argumentmap
will provide and combine it with an initial argument3
currying unwinds a single higher-arity function into a series of chained unary functions
- Partial application feels suited to multiple arguments
- You provide one or more arguments in the first call and the next call expects all the remaining arguments
- Curried functions will always only expect a single argument
- More ergonomic if working with single arguments
- Both use closure to remember arguments
function curry(fn,arity = fn.length) {
return (function nextCurried(prevArgs){
return function curried(nextArg){
var args = [ ...prevArgs, nextArg ];
if (args.length >= arity) {
return fn( ...args );
}
else {
return nextCurried( args );
}
};
})( [] );
}
- Super interesting
curry
utility - Good reminder that
fn.length
is not a very reliable indicator of arity, so may need to be explicitly passed in
curry
:
- takes core function (
fn
) and sets it aside - runs
nextCurried
immediately, which is indifferent to the current invocation's inputnextCurried
exposes theprevArgs
to the current invocation
- returns
curried
as the function which a program will invoke - uses
arity
as a breaking condition to determine when the core function (fn
) is to be invoked
var curriedAjax = curry( ajax );
var personFetcher = curriedAjax( "http://some.api/person" );
var getCurrentUser = personFetcher( { user: CURRENT_USER_ID } );
getCurrentUser( function foundUser(user){ /* .. */ } );
// (5 to indicate how many we should wait for)
var curriedSum = curry( sum, 5 );
curriedSum( 1 )( 2 )( 3 )( 4 )( 5 ); // 15
- A manual currying (i.e. without the
curry()
utility) could be visualized below
function curriedSum(v1) {
return function(v2){
return function(v3){
return function(v4){
return function(v5){
return sum( v1, v2, v3, v4, v5 );
};
};
};
};
}
curriedSum = v1 => v2 => v3 => v4 => v5 => sum( v1, v2, v3, v4, v5 );
- Note that the arrow syntax version looks very similar to mathematical notation or Haskell
- One reason people might prefer it
- Great section worth clicking through to
sum(1)(2)(3) // curried function
partial(sum,1,2)(3) // partial application
sum(1,2,3) // common style
-
Why use FP techniques when the call-site can become more confusing?
-
1: Frees us from needing to know all the arguments for a function call at a single point in the codebase
-
2: Composition of functions is easier with only one argument (applies to currying)
-
3: Most critically, splitting out the generalized from the specialized aspects of a function is an abstraction that improves readability
ajax(
"http://some.api/person",
{ user: CURRENT_USER_ID },
function foundUser(user){ /* .. */ }
);
- Common style where everything needs to be provided at once
var getCurrentUser = partial(
ajax,
"http://some.api/person",
{ user: CURRENT_USER_ID }
);
// later
getCurrentUser( function foundUser(user){ /* .. */ } );
- Partial application allows the call site of
getCurrentUser
to be focused on the specific argument that matters at that point (here the callback)- Avoids "cluttering-up" the call-site with irrelevant info
- Naming
getCurrentUser
is more informative than just callingajax(..)
and it abstracts the earlier arguments into a terse summary - Key benefit here is that the two locations are easier to reason about on their own
That's what abstraction is all about: separating two sets of details -- in this case, the how of getting a current user and the what we do with that user -- and inserting a semantic boundary between them, which eases the reasoning of each part independently.
- Argument ordering can become a pain when using partial application or currying
- For instance, if you want to add the 3rd argument but haven't yet added the 2nd argument into the curried function
function partialProps(fn,presetArgsObj) {
return function partiallyApplied(laterArgsObj){
return fn( Object.assign( {}, presetArgsObj, laterArgsObj ) );
};
}
function curryProps(fn,arity = 1) {
return (function nextCurried(prevArgsObj){
return function curried(nextArgObj = {}){
var [key] = Object.keys( nextArgObj );
var allArgsObj = Object.assign(
{}, prevArgsObj, { [key]: nextArgObj[key] }
);
if (Object.keys( allArgsObj ).length >= arity) {
return fn( allArgsObj );
}
else {
return nextCurried( allArgsObj );
}
};
})( {} );
}
- Using objects as both the arguments and in parameter destructuring we can overcome ordering specificity
function foo({ x, y, z } = {}) {
console.log( `x:${x} y:${y} z:${z}` );
}
var f1 = curryProps( foo, 3 );
var f2 = partialProps( foo, { y: 2 } );
f1( {y: 2} )( {x: 1} )( {z: 3} );
// x:1 y:2 z:3
f2( { z: 3, x: 1 } );
// x:1 y:2 z:3
- Point-free style where point refers to the function's parameter input
function double(x) {
return x * 2;
}
[1,2,3,4,5].map( function mapper(v){
return double( v );
} );
- Because the
mapper
anddouble
function signatures are the same, we can write this in point-free style
The parameter ("point") v can directly map to the corresponding argument in the double(..) call.
[1,2,3,4,5].map( double );
- Worth clicking through to read
- One new take away for me is that utilities (
unary(..)
,identity(..)
, andconstant(..)
) are a key part of FP - Seeing some of the hand-rolled utilities is great for learning
function not(predicate) {
return function negated(...args){
return !predicate( ...args );
};
}
// or the ES6 => arrow form
var not =
predicate =>
(...args) =>
!predicate( ...args );
- In particular this recurring higher-order function pattern is great to become more comfortable with
- BUT I suspect that Ramda or a similar library would provide me with tried and tested utilities that I'd be happy to use rather than trying to spin my own
- In particular, a utility like the one converting a function to string and then using a regular expression to pull off the argument order, which would only work 80% of the time, seems like something I'd happily leave to a trusted 3rd-party library
function words(str) {
return String( str )
.toLowerCase()
.split( /\s|\b/ )
.filter( function alpha(v){
return /^[\w]+$/.test( v );
} );
}
function unique(list) {
var uniqList = [];
for (let v of list) {
if (uniqList.indexOf( v ) === -1 ) {
uniqList.push( v );
}
}
return uniqList;
}
- Two utility functions (or lego blocks)
var wordsUsed = unique( words( text ) );
- Utilities used efficiently together
function uniqueWords(str) {
return unique( words( str ) );
}
- Functions composed into a new utility (compound lego block)
function compose2(fn2,fn1) {
return function composed(origValue){
return fn2( fn1( origValue ) );
};
}
// or the ES6 => form
var compose2 =
(fn2,fn1) =>
origValue =>
fn2( fn1( origValue ) );
var uniqueWords = compose2( unique, words );
- Parameters are right to left
- Common convention in FP libraries with a
compose
function - Easiest way to remember is the parameter order matches how the nested functions appear in code
- Common convention in FP libraries with a
function compose(...fns) {
return function composed(result){
// copy the array of functions
var list = [...fns];
while (list.length > 0) {
// take the last function off the end of the list
// and execute it
result = list.pop()( result );
}
return result;
};
}
while
loop is an interesting little bit of logic- Not sure I'd agree that
result
is the best name for the parameter (why call an input a result?) result = list.pop()( result )
is a satisfying line though as it both passes a value into a function and sets the return of that operation as the value to be used in the next step
- Not sure I'd agree that
function skipLongWords(list) { /* .. */ }
var filterWords = partialRight( compose, unique, words );
var biggerWords = filterWords( skipShortWords );
var shorterWords = filterWords( skipLongWords );
biggerWords( text );
// ["compose","functions","together","output","first",
// "function","input","second"]
shorterWords( text );
// ["to","two","pass","the","of","call","as"]
- Pretty cool to see how useful
partialRight
is here- Suppose that makes sense since the first argument would be the last to be executed in the composed functions
- Adding all the final arguments and allowing only the first one to be slotted in
- Likely wouldn't implement my own
compose
function, would use a library's- But good to follow the logic
- pipe = same as
compose
but left to right- Comes from Unix/Linux pipe (
|
operator) - i.e.
ls -la | grep "foo" | less
- Comes from Unix/Linux pipe (
var pipe = reverseArgs( compose );
- Can simply reverse arguments as above, or could use
shift
instead ofpop
var biggerWords = compose( skipShortWords, unique, words );
var biggerWords = pipe( words, unique, skipShortWords );
- Might be a bit more intuitive to read left to right the order the composed functions with execute in
var filterWords = partialRight( compose, unique, words );
var filterWords = partial( pipe, words, unique );
- Partial application also a bit more intuitive without needing to take the mental tick to understand
partialRight
- Fantastic section worth sharing directly
- Abstraction separates a program into subsets
- Contained subsets are more readable
making it possible for the programmer to focus on a manageable subset of the program text at any particular time.
We're not abstracting to hide details; we're separating details to improve focus.
- Declarative code abstracts the what from the how
- At the call-site we can focus on the what
- Destructuring is a good example of increased declarative ability ES6 introduced
function getData() {
return [1,2,3,4,5];
}
// imperative
var tmp = getData();
var a = tmp[0];
var b = tmp[3];
// declarative
var [ a ,,, b ] = getData();
- Array destructing as declarative abstraction
// imperative
function shorterWords(text) {
return skipLongWords( unique( words( text ) ) );
}
// declarative
var shorterWords = compose( skipLongWords, unique, words );
- Abstraction and composition are often framed as something to undertake once code is repeated
- i.e., I've used this twice, so I should abstract it
- Often abstraction is justified by DRY
- Point that even if just using a function once, there's a lot of benefit that abstraction can bring by enabling a more declarative code style
shorterWords( text );
- Great example where the call-site can be entirely focused on what the program should do, rather than how it does it
// given: ajax( url, data, cb )
var getPerson = partial( ajax, "http://some.api/person" );
var getLastOrder = partial( ajax, "http://some.api/order", { id: -1 } );
getLastOrder( function orderFound(order){
getPerson( { id: order.personId }, function personFound(person){
output( person.name );
} );
} );
- Starting point with the aim to remove the points of the
order
andperson
parameter references- Steps in example are worth clicking through to
function prop(name,obj) {
return obj[name];
}
- Utility helpful in initial aim of removing
person
as a point
var extractName = partial( prop, "name" );
- This will now just need to be provided an object (final argument)
- Somewhat awkward that the argument
"name"
is the same term used as the parameter value inprop
- The parameter in
prop
could bepropName
- Somewhat awkward that the argument
function setProp(name,obj,val) {
var o = Object.assign( {}, obj );
o[name] = val;
return o;
}
- Inverse utility to
prop
- Good model of setting a property and keeping an immutable object (New object via
Object.assign
)
- Good model of setting a property and keeping an immutable object (New object via
output( person.name );
// ->
var outputPersonName = compose( output, extractName );
- Composing the new
extractName
function
function makeObjProp(name,value) {
return setProp( name, {}, value );
}
- Feels like a pretty granular function, but suppose it locks in the immutable approach in
setProp
- This is
objOf
in Ramda
- This is
var getPerson = partial( ajax, "http://some.api/person" );
var getLastOrder =
partial( ajax, "http://some.api/order", { id: -1 } );
var extractName = partial( prop, "name" );
var outputPersonName = compose( output, extractName );
var processPerson = partialRight( getPerson, outputPersonName );
var personData = partial( makeObjProp, "id" );
var extractPersonId = partial( prop, "personId" );
var lookupPerson =
compose( processPerson, personData, extractPersonId );
getLastOrder( lookupPerson );
- Final point-free refactor
- Uses loads of the composition techniques
- (Didn't capture all the steps in these notes. See full chapter)
- Function composition in JS relies on functions being unary
- This is because JS functions can only return a single value
- Each composed function takes a single input from the preceding functions single output
Composition is declarative data flow
- Notion of routing data through the program
- Ease of readability = clear cause and effect in code
// Clear cause and effect
function foo(x) {
return x * 2;
}
var y = foo( 3 );
// Same functionality but with a side effect and less clear cause and effect relationship
function foo(x) {
y = x * 2;
}
var y;
foo( 3 );
-
Free variable = a variable a function references outside itself (as above)
-
If you don't know if a function has side effects, then you can't use it without finding out
- You need to look at the function definition
- If there are side effects, then you also need to understand the other aspect(s) of the program affected
- side effects = less ease in readability
The readability of a side effecting function is worse because it requires more reading to understand the program.
- Nice point that choosing to use side effects means forcing readers of your code to mentally execute your program