Think you are confident with javascript? Here's 20 Days of restudying javascript.
- 20-days-of-restudying-javascript
- Table of Contents
- Topics
- Prerequisites
- Day 1
- Day 2
- Day 3
- Day 4 (Started learning functional javascript)
- Day 5
- Day 6
- Day 7
- Day 8
- Day 9
- Day 10
Created by gh-md-toc
- Preferrable practices in javascript
- Functional & declartive programming using javascript
- In-depth inspection:
Symbol
,WeakMap
,WeakSet
,Object
,prototype
,this
,bind
,class
,async
,await
,yield
and more.
- Have read the latest edition of "Learning javascript" at least three times.
- Have read "You Don't Know JS" at least once (roughly is ok)
- Have coded substantial amount of javascript already
- Now wanting to get some really fine techniques on javascript
Read Airbnb's javascript style guide:
- ⌛: 30 ~ 60 mins
- From 1. Types
- To 7.6 Never use arguments, opt to use rest syntax ...
1. Array.prototype.slice()
in 4.5:
The slice()
method returns a shallow copy of a portion of an array into a new array object selected from begin to end (end not included). The original array will not be modified.
- Q:
slice
works on objects? - A: Slice method can also be called to convert Array-like objects / collections to a new Array.. So you could do something like:
Remember you need to specify the length, and the object keys should be indices. The following would result in nothing but an empty array:
const info = Array.prototype.slice.call({0: "Korea", 1: "KST", length: 2}) // ["Korea", "KST"]
But anyways, it's not recommended to useconst info = Array.prototype.slice.call({home: "Korea", time: "KST"}) // []
Array.prototype.slice
, butArray.from
.
2. Array.from
in 4.6
- Q: So what can
Array.from
actually do with mapping? - A: This:
Array.from
can receive a mapping function as the second argument, as such:const mapped = Array.from([1,2,3], elem=>elem*2) // 2,4,6
Read Airbnb's javascript style guide:
- ⌛: 30 ~ 60 mins
- From 7.7 Use default parameter syntax...
- To 15.5 Use braces to create blocks in case and default clauses...
- 14.2 Anonymous function expressions hoist their variable name, but not the function assignment.
- 14.3 Named function expressions hoist the variable name, not the function name or the function body.
Read Airbnb's javascript style guide:
- ⌛: 45 ~ 60 mins
- From 15.6 Ternaries should not be nested...
- To 30.2 No, but seriously: (The end)
- 18.4 You can use
FIXME
orTODO
to annotate something in the comment. - 19.6 Use indentation when making long method chains...
- 21.1 Use semicolons.: You know, I don't really use semicolons in Javascript because the code tends to look cleaner. But airbnb certainly has suggested possible grounds for this, such as:
- "rules will become more complicated as new features become a part of JavaScript. Explicitly terminating your statements and configuring your linter to catch missing semicolons will help prevent you from encountering issues."
- But doesn't
prettier
do the job for filling out all the semicolons? I will have to dig into this a bit more.
- 22.2 Type casting for strings: don't use new keyword, but just 'String', because using the new keyword will let javascript recognize the variable as an object. (typeof)
- 23.4 Do not use trailing or leading underscores.
- 25.1 When attaching data payloads to events (whether DOM events or something more proprietary like Backbone events), pass an object literal (also known as a "hash") instead of a raw value.
- 29.1 Use
Number.isNaN
instead ofisNaN
(same forNumber.isFinite
)
- ⌛: 20 mins Gathered up some resources
- Short read on functional javascript: the good parts
- JavaScript Functional Programming Cookbook (ES6)
- Real world functional programming in JS: Tips and guidelines for real world functional code-bases in JS
- ALDO FP Guide
- Awesome functional programming (Really comprehensive!)
- List of awesome fp in js
- Functional JavaScript Resources
- Curation of resources in FP in javascript
And so, I will look at those two books mentioned in Day 4.
- ⌛: 30 ~ 60 mins
-
Read 'Chapter 1: What Ever Are We Doing?' of Mostly adequate guide to FP
-
Many times we tend to write many more lines code than needed. If you think, and especially inFP way, you can greatly reduce the code to more exact, concise one.
-
Associative, commutative, identity, distributive rules work to further reduce the code (if you have done some math classes), like below (from chapter 1):
const add = (x, y) => x + y; const multiply = (x, y) => x * y; /* associative * Wiki link: https://en.wikipedia.org/wiki/Associative_property#Definition * Example: (x ∗ y) ∗ z = x ∗ (y ∗ z) for all x, y, z in S.(Multiplication) */ add(add(x, y), z) === add(x, add(y, z)); /* commutative * Wiki link: https://en.wikipedia.org/wiki/Commutative_property#Mathematical_definitions * Example: x * y = y * x for all x,y in S. */ add(x, y) === add(y, x); /* identity * Wiki link: https://en.wikipedia.org/wiki/Identity_function#Definition * Example: f(x) = x for all x in certain domain. */ add(x, 0) === x; /* distributive * Wiki link: https://en.wikipedia.org/wiki/Distributive_property#Definition * Example: x * ( y + z ) = (x * y) + (x * z) */ multiply(x, add(y,z)) === add(multiply(x, y), multiply(x, z));
apply these rules to reduce the code:
// Original line add(multiply(flockB, add(flockA, flockC)), multiply(flockA, flockB)); // Apply the identity property to remove the extra add // (add(flockA, flockC) == flockA) add(multiply(flockB, flockA), multiply(flockA, flockB)); // Apply distributive property to achieve our result multiply(flockB, add(flockA, flockA));
-
And that's only the beginning.
-
Read 'Chapter 2: First class functions' of Mostly adequate guide to FP
-
What is a first class function?:
A programming language is said to have First-class functions when functions in that language are treated like any other variable. For example, in such a language, a function can be passed as an argument to other functions, can be returned by another function and can be assigned as a value to a variable. (From Mozilla)
We can treat functions like any other data type and there is nothing particularly special about them - they may be stored in arrays, passed around as function parameters, assigned to variables, and what have you. (From the book)
-
Often we write more code than needed:
// this line ajaxCall(json => callback(json)); // is the same as this line ajaxCall(callback); // so refactor getServerStuff const getServerStuff = callback => ajaxCall(callback); // ...which is equivalent to this const getServerStuff = ajaxCall; // <-- look mum, no ()'s
Now you can run
ajaxCall
with as many arguments as you want to put in. Just treat it like any other variables. That's the point. -
Note on naming functions: don't be too specific. Be broader so that you can use them for any other future projects. Make it reusable.
-
Read 'Chapter 03: Pure Happiness with Pure Functions' of Mostly adequate guide to FP
-
⌛: 30 ~ 60 mins
A pure function is a function that, given the same input, will always return the same output and does not have any observable side effect.
This is exactly the same principle for the definition of function in the world of real math: each input maps exactly to one (not several) output only.
To make it easy to write a pure function, do not rely on the variables from outside world.
You could 'freeze' or use other tools like immutable.js
to make sure that constant variables stay constant inside a function.
Pure functions can be cached by input. This technique is sometimes called memoization:
In computing, memoization or memoisation is an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again.
So this would be a simple code for memoize
:
const memoize = (f) => {
const cache = {};
return (...args) => {
const argStr = JSON.stringify(args);
console.log(argStr)
console.log(cache) // for help
cache[argStr] = cache[argStr] || f(...args);
return cache[argStr];
};
};
This is only really a simple case for memoization. You simply store in cache
object.
Let's have a multiply function memoized:
multiply=memoize((x,y)=>x*y)
1st time you run it, you get:
multiply(1,2)
[1,2]
{}
But the 2nd time you call it:
multiply(1,2)
[1,2]
{[1,2]: 2}
Your input and the corresponding output were stored in the cache and those were used instead of calculating the result by running the function once more.
Because if the function were impure, there's no point in saving the function output in the cache, as it does not have a guarantee that it is going to be the same each time.
// impure
const signUp = (attrs) => {
const user = saveUser(attrs);
welcomeUser(user);
};
// pure
const signUp = (Db, Email, attrs) => () => {
const user = saveUser(Db, attrs);
welcomeUser(Email, user);
};
- Pure function is honest about its dependencies (signature). You know exactly what to use. It's more informative.
- You must use dependencies as function arguments. Otherwise you've got no choice. It's more flexible and at the same time, self-documenting. But look at the impure version. You modify database and more without relying on the arguments. The logic is hidden behind.
- Pure function, likewise, is portable. You can serialize/send over pocket. You can run your code anywhere.
Outputs are always predictable. Therefore you don't have to:
mock a "real" payment gateway or setup and assert the state of the world after each test.
You just need input and predict (assert) output in your tests.
Referential transparency. Codes can be substituted around because you already know that output of function will stay the same anyways.
You can use this property to easily refactor functions. For more, see this part of the book.
Pure functions do not need to access shared memory and cannot have a race condition due to side effects.
-
⌛: 30~60 mins
Currying is:
In mathematics and computer science, currying is the technique of translating the evaluation of a function that takes multiple arguments into evaluating a sequence of functions, each with a single argument.
For example,
curry((x,y) => x*y)
will get curried down to
x => y => x*y
Similarly,
curry((x,y,z) => x*y*z)
will get curried down to
x => y => => z => x*y*z
Simple, right? Here's a definition of curry
:
// curry :: ((a, b, ...) -> c) -> a -> b -> ... -> c
function curry(fn) {
const arity = fn.length;
return function $curry(...args) {
if (args.length < arity) {
return $curry.bind(null, ...args);
}
return fn.call(null, ...args);
};
};
Let's inspect it. First change the code so that you can log outputs:
function curry(fn) {
const arity = fn.length;
console.log(`arity: ${arity}`)
return function $curry(...args) {
console.log(`args: ${args}
args.length: ${args.length}`)
if (args.length < arity) {
return $curry.bind(null, ...args);
}
return fn.call(null, ...args);
};
};
Now run:
multiply=curry((x,y,z) => x*y*z)
It of course logs:
arity: 3
Then run the function:
multiply(1,2,3)
Then it logs
args:1,2,3
args.length: 3
// returns
6
Cool. But what if args.length < arity
? Let's try this out.
const multiplyLast=multiply(1,2)
// logs
args: 1,2
args.length: 2
Now
multiplyLast(3)
// logs
args: 1,2,3
args.length: 3
// returns
6
Wow. yeah. So you can actually stop the execution of function at certain point by manipulating the number of arguments.
Now you can understand more of such functions:
const match = curry((what, s) => s.match(what));
const replace = curry((what, replacement, s) => s.replace(what, replacement));
const filter = curry((f, xs) => xs.filter(f));
const map = curry((f, xs) => xs.map(f));
Look at these usages. Beautiful. You can mix them around, plug them into each other, do whatever you want essentially.
match(/r/g, 'hello world'); // [ 'r' ]
const hasLetterR = match(/r/g); // x => x.match(/r/g)
hasLetterR('hello world'); // [ 'r' ]
hasLetterR('just j and s and t etc'); // null
filter(hasLetterR, ['rock and roll', 'smooth jazz']); // ['rock and roll']
const removeStringsWithoutRs = filter(hasLetterR); // xs => xs.filter(x => x.match(/r/g))
removeStringsWithoutRs(['rock and roll', 'smooth jazz', 'drum circle']); // ['rock and roll', 'drum circle']
const noVowels = replace(/[aeiou]/ig); // (r,x) => x.replace(/[aeiou]/ig, r)
const censored = noVowels('*'); // x => x.replace(/[aeiou]/ig, '*')
censored('Chocolate Rain'); // 'Ch*c*l*t* R**n'
instead of
const allTheChildren = elements => map(elements, getChildren);
you can do
const getChildren = x => x.childNodes;
const allTheChildren = map(getChildren);
You can just directly transform the function into something that works on bunch of elements (array) instead of one element.
-
Read up to '"Pointfree" Section of Chapter 05: Coding by Composing' of Mostly adequate guide to FP
-
⌛: 45~60+ mins
here's a simple version of compose function that only composes two functions:
const compose = (f, g) => x => f(g(x));
simple. calls g first and then plugs that into f.
But what if you wanted to chain like 10 functions through compose?
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
here it is.
So first, we have to know about reduceRight
:
The reduceRight() method applies a function against an accumulator and each value of the array (from right-to-left) to reduce it to a single value.
Ok. So (v,f)=>f(v)
is actually an accumulator and x
accounts for the current value.
Example:
const array1 = [[0, 1], [2, 3], [4, 5]].reduceRight(
(accumulator, currentValue) => {
console.log(`acc: ${accumulator}`)
console.log(`current: ${currentValue}`)
return accumulator.concat(currentValue)
}
);
console.log(array1);
// expected output: Array [4, 5, 2, 3, 0, 1]
> "acc: 4,5"
> "current: 2,3"
> "acc: 4,5,2,3"
> "current: 0,1"
> Array [4, 5, 2, 3, 0, 1]
Anyways, let's see what's happening again in that compose
function.
Let's kindly rewrite it with familiar terms:
const compose = (...fns) => initialValue => fns.reduceRight((accumulator, fn) => fn(accumulator), initialValue)
-
Ok. So
(value, fn) => fn(value)
is the callback andinitialValue
is literally just the initial value. Get to know thatreduceRight
takes two arguments:callback
andinitialValue
, the second being optional, but specified incompose
function. -
Notice that
(accumulator, fn) => fn(accumulator)
simply returns an output of a function with an input which isaccumulator
. Actually,fn
is thecurrentValue
because for each time a differentfn
is called from...fns
, from right to left. -
Now when
compose
is run, theinitialValue
will be plugged into the place ofaccumulator
in the callback, and the rightmost function supplied in...fns
will receiveaccumulator
to return an output. -
Now when the value is calculated and stored in
accumulator
, again the next functionfn
(second rightmost element infns
) will be called with it. -
This process will be repeated until
fns.length === 0
. -
Also, notice that leaving
initialValue
field empty will simply not make it work because if so, it will grab the rightmost element fromfns
array, which is a function, not some value you want. -
Anyways, this way, you are going to continuously execute a function at a time and insert its output to the next function until you use all functions provided.
here's an example slightly modified from the book.
const toUpperCase = x => x.toUpperCase();
const exclaim = x => `${x}!`;
const head = x => x[0];
const loudFirst = compose(
toUpperCase,
exclaim,
head
)
loudFirst(['composing', 'is', 'cool'])
// COMPOSING!
Then what's different for reduce
? Same logic, but only left-to-right.
Just look at the example from MDN docs:
var a = ['1', '2', '3', '4', '5'];
var left = a.reduce(function(prev, cur) { return prev + cur; });
var right = a.reduceRight(function(prev, cur) { return prev + cur; });
console.log(left); // "12345"
console.log(right); // "54321"
simple. right?
pipe(...fns)
is just an opposite of compose
function. Sometimes it's more intuitive to put functions in the order of execution. So for example:
instead of
const loudFirst = compose(
toUpperCase,
exclaim,
head
)
you are going to write
const loudFirst = compose(
head
exclaim,
toUpperCase
)
pipe
is really similar to compose
:
pipe = (...fns) => x => fns.reduce((v, f) => f(v), x)
Actually, the only different thing is it uses reduce
instead of reduceRight
. This allows you to calculate from left to right, not right to left. That's why the order of function inputs are opposite.
If your function is pointfree (it's also called tacit programming), it does not specify the arguments it uses.
From Wikipedia:
Tacit programming, also called point-free style, is a programming paradigm in which function definitions do not identify the arguments (or "points") on which they operate.
Ref article (there are some codes and sentences used from here): https://medium.freecodecamp.org/how-point-free-composition-will-make-you-a-better-functional-programmer-33dcb910303a
There is no additional anonymous callback when doing point-free composition. No function keyword, no arrow syntax => . All we see are function names.
The consequence of writing code this way is a lot of small functions with intention revealing names. Naming these small functions requires time, but if it’s done well, it will make the code easier to read.
So rather than
fetchTodos().then((todos) => {
let topTodos = getTopPriority(todos);
render(topTodos);
}).catch((error) => {
console.error("There was an error :" + error.status);
});
function getTopPriority(todos){}
function render(todos){}
You do
fetchTodos()
.then(getTopPriority)
.then(render)
.catch(handleError);
prop()
A general purpose function to retrieve a prop.
let titles = books.map(prop("title"));
function prop(propName){
return function getProp(obj){
return obj[propName];
}
}
unary()
Takes only one (the first) argument out from multiple arguments.
let numbers = [1,2,3,4,5,6];
numbers.forEach(console.log);
//1 0 (6) [1, 2, 3, 4, 5, 6]
//2 1 (6) [1, 2, 3, 4, 5, 6]
//...
// by default inserting a function ref will input all default arguments, so it's equivalent to:
numbers.forEach((item, index, array) => console.log(item, index, array));
// or easily
numbers.forEach((...all) => console.log(...all))
// so you do this
function unary(fn){
return function unaryDecorator(first){
return fn.call(this, first);
}
}
numbers.forEach(unary(console.log));
//1 2 3 4 5 6
- Factory function: you do not lose
this
context - Class: you lose it. You have to manually
bind
it - See example code
-
Read up to The end of Chapter 6: 'Example Application'
-
⌛: 30 mins
It's just stating the specification of what we would like as a result. It's about what, not how.
Some perks of declarative coding:
- More freedom to an expression
- Clearer and more concise
- May be optimized faster by JIT
Some example:
const cases = [1, 4, 5, 2, 0, 3, 9, 8]
// An imperative coding example
const someOverFive = (cases) => {
for (let i = 0; i < cases.length; i++){
if (cases[i] > 5){
return true
}
}
return false
}
someOverFive(cases)
You could do this... but,
const cases = [1, 4, 5, 2, 0, 3, 9, 8]
cases.some(c => c > 5)
what about this game changer.
If you are not sure about the difference between impure and pure functions, take a glimpse at this stackoverflow answer:
- The return value of the pure functions solely depends on its arguments Hence, if you call the pure functions with the same set of arguments, you will always get the same return values.
- They do not have any side effects like network or database calls
- They do not modify the arguments which are passed to them
- The return value of the impure functions does not solely depend on its arguments Hence, if you call the impure functions with the same set of arguments, you might get the different return values For example, Math.random(), Date.now()
- They may have any side effects like network or database calls
- They may modify the arguments which are passed to them
These are impure functions.
const Impure = {
getJSON: curry((callback, url) => $.getJSON(url, callback)),
setHtml: curry((sel, html) => $(sel).html(html)),
trace: curry((tag, x) => { console.log(tag, x); return x; }),
};
Why?
getJSON
uses$
which is a global variable. Also it fetches data over a network.setHtml
depends on$
which is not from its argument as well.trace
callsconsole.log
.
- Read up to The End of Chapter 7: Hindley-Milner and Me
- ⌛: 20 mins
- if you are already familiar with Typescript or any other strictly typed languages, you can probably skip day 10.
- There is a type system that is quite similar to TypeScript, called Hindley-Milner.
- It basically works like this:
// capitalize :: String -> String const capitalize = s => toUpperCase(head(s)) + toLowerCase(tail(s));
capitalize :: String -> String
means a function that takes a string and returns a string.- More complex (but still easy to understand) example could be returning a function from a function:
// add :: Number -> Number -> Number const add = x => y => x + y; // match :: Regex -> (String -> [String]) const match = curry((reg, s) => s.match(reg)); // onHoliday :: String -> [String] const onHoliday = match(/holiday/ig);
- Type variables can be used in the type signature. Type variables can be denoted by any letters (like a, b, c) or words:
// id :: a -> a const id = x => x; // map :: (a -> b) -> [a] -> [b] const map = curry((f, xs) => xs.map(f));
- Type variable can introduce a property called parametricity: a function will act on all types in a uniform manner. When a function acts like this, it is said to be a "parametrically polymorphic function". In easier terms, it's just a generic function, which should come in very handy if you are already used to statically typed languages like Java, C#, Golang, Typescript, etc.
- One example of a parametrically polymorphic function is
head
:Its type signature is:const head = ([first]) => first
And it can be rewritten in Typescript, with generics, as:head :: [a] -> a
type Head = <T>(arr: T[]) => T // for example, take in a number array const head: Head<number> = ([first]) => first
- In short, parametricity is just a fancy term to describe a generic function.
- Read up to The End of Chapter 8: Tupperware
- ⌛: 20 mins
So far we've studided how and when to use pure functions. Now we will study how to handle impure aspects of the code.