პირადი პროექტი
ინტერაქტიული კურსი ფუნქციურ პროგრამირებაზე JS-ში
კურსი კოდის რედაქტორით პირდაპირ ბრაუზერში: დაწერე და გაუშვი მაგალითები სუფთა JS-ში ბიბლიოთეკებით ramda და sanctuary. FP ახსნილი პრაქტიკაში.
Playground
დაწერე და გაუშვი JavaScript აქვე — Ramda (R) და Sanctuary (S) ხელმისაწვდომია.
01 Who this course is for
This course is written primarily for developers with JS experience who want to broaden their horizons and write more reliable, declarative, and maintainable code in a functional style.
02 Who this course is not for
This course is not suitable for beginner developers who are only just learning the basics of JS development.
03 Imperative vs declarative
Imperative code tells you "how you do it". In other words, you have to dig into the code itself.
Declarative code tells you what your code does; it is more often modular, and you do not need to dig into the whole codebase. If changes are needed, you make them in a specific section of the code in a modular way.
// например получить все числа больше 3 из текущего массива
// Императивный код
// ========
let result = [] // массив результирующий
const numbers = [1, 2, 3, 4, 5] // текущий массив
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] > 3) {
result.push(numbers[i])
}
}
console.log(result) // 4, 5
// =========
// Декларативный код
// =========
// isGt3 :: Number -> Boolean
const isGt3 = number => number > 3
const result = numbers.filter(isGt3) // мы не вникаем в реализацию а просто читаем название
console.log(result) // 4, 5 тот же результат более декларативен
// =========
// Мы можем повысить слои абстракции (и тут Остапа понесло))) )
// =========
// не пугайтесь если сразу непонятно, поймете дальше
// filter :: (* -> Boolean) -> [a] -> [a]
const filter = (predicat, arr) => arr.filter(predicat)
// filterBy :: (* -> Boolean) -> [a] -> [a]
const filterBy = (predicat) => (arr) => filter(predicat, arr)
// gt :: Number -> (Number -> Boolean) -> Boolean
const gt = pointer => number => number > pointer
// теперь мы создаем любые функции предикаты
// для фильтрации
// gt2 :: Number -> (Number -> Boolean)
const gt2 = gt(2) // предикат для фильтрации чисел больше 2
const gt3 = gt(3) // предикат для фильтрации чисел больше 3
// а вот теперь мы можем фильтровать массив по любым предикатам
const arr = [1, 2, 3, 4, 5]
const more2 = filterBy (gt2) (arr) // получаем числа больше 2
const more3 = filterBy (gt3) (arr) // получаем числа больше 3
// мы можем также главную функцию фильтрации подготовить для конвееров
// не вызывая сразу ее
// filterByGt2 :: [a] -> [a]
const filterByGt2 = filterBy (gt2) // ожидает массива
// filterByGt2 (arr) из названия мы уже понимаем что идет фильтрация чисел больше 2
const result = filterByGt2 (arr) // получаем готовый результат числа больше 2
// ======04 Functional programming
Functional programming is high-level, declarative development based on pure functions. Functional programming is a programming paradigm that relies on functions as its primary building blocks.
Pure functions are functions that return the same result for the same arguments and produce no side effects.
// ==== Внимание ===
// Эти примеры Вам пока могут казаться крайне запутанными и непонятными
// Не пугайтесь и читайте дальше
// наша цель
// 1. Получить пользователей со статусом онлайн
// 2. Старше 20 лет
// 3. Первого пользователя из списка
// 4. Получить его имя
// Пример функционального кода
// импорт готовых утилит с библиотеки
import { pipe, prop, equals, filter, head, gt } from 'sanctuary';
// fetchAllUsers :: * -> Promise a
const fetchAllUsers = () => new Promise(resolve => resolve([
{
name: 'John',
status: 'online',
age: 25
},
{
name: 'Alex',
status: 'offline',
age: 18
},
{
name: 'Angel',
status: 'online',
age: 18
}
]))
const ONLINE = 'online'
// isStatus :: String -> (a -> Boolean)
const isStatus = status => pipe([
prop('status'), // берем содержимое поля статус у пользователя
equals(status), // сравниваем поле юзера со статусом установленным выше
])
// moreAge :: Number -> (a -> Boolean)
const moreAge = age => pipe([
prop('age'), // берем содержимое поля возраст
gt(age), // поле юзера было больше age
])
// isStatusOnline :: a -> Boolean
const isStatusOnline = isStatus(ONLINE)
// moreAge20 :: a -> Boolean
const moreAge20 = moreAge(20)
// getName :: a -> String
const getName = prop('name')
// Внимание необязательно создавать чрезмерную абстракцию
// это иногда может запутать коллег
// В идеале конечно лучше обернуть в монаду для защиты от падений композиции
// в случае если с сервера прилетит, что попало и это будет настоящей защитой
// а не псевдо как в typescript
// searchOnlineUsers :: [a] -> a
const searchOnlineUsers= pipe([
filter(isStatusOnline), // получаем только пользователей онлайн
filter(moreAge20), // получаем пользователей старше 20 лет
head, // получаем первого пользователя из списка
getName // получить имя этого пользователя
])
// start программы
const onlineUsersByAge = await fetchAllUsers().then(searchOnlineUsers)
console.log(onlineUsersByAge) // 'John'05 Pure functions
Pure functions are extremely important and are usually idempotent, but not always. If you need side effects, group them into separate functions and keep them apart from pure functions. Every program, especially on the web, needs side effects (mutating the DOM tree, requests to the server (fetch), reading from a file (readFile), and so on).
// НЕчистая функция
// ===============================
let user = { name: 'John', age: 25 }
const changeAge = () => user.age = 30
// мы мутируем внешнюю переменную и создаем сайд эффекты
// программа будет везти себя непредсказуемо так как с этой
// переменной могут работать несколько функций и отловить ошибку будет сложнее
changeAge()
console.log(user) // { name: 'John', age: 30 }
// ===============================
// Такой код допустим, если мы создаем независимые модули с собственным замыканием
// ===============================
// useChangeUser :: Object -> Object
const useChangeUser = (user) => {
let userBase = structuredClone(user) // обязательно клонируем
const changeAge = (newAge) => userBase.age = newAge
// для работы снаружи с данными
return {
userBase,
changeAge
}
}
// Но даже в этом случае лучше делать так
// useChangeUser :: Object -> Object
const useChangeUser = (user) => {
let userBase = structuredClone(user) // обязательно клонируем
const changeAge = (newAge) => userBase = ({
...userBase, // или structuredClone(userBase) или JSON.parse(JSON.stringify(user))
age: newAge
})
// для работы снаружи с данными
return {
userBase,
changeAge
}
}
// ===============================
// А теперь давайте поправим пример выше
// ===============================
let user = { name: 'John', age: 25 }
// Чистая функция
// changeAge :: Number -> Object -> Object
const changeAge = (newAge, user) => ({
...user,
age: newAge
})
// улучшим
// или если требуется глубокое клонирование и каррирование
// changeAge :: Number -> Object -> Object
const changeAge = (newAge) => (user) => ({
...structuredClone(user), // или JSON.parse(JSON.stringify(user))
age: newAge
})
// тут мы проверим все
const changedUser = changeAge (30) (user)
console.log(user) // { name: John, age: 25 } сохранил первоначальное состояние
console.log(changedUser) // { name: John, age: 30 } получили нового пользователя без сайд эффектов
// ===============================06 Hindley–Milner type notation
You have already come across Hindley–Milner type notation in the code — above functions, in comment blocks.
This type notation is used in Haskell and other languages with a functional bent. In JS it helps us write more abstract code, and it also helps maintain that code.
It explains which argument types a function accepts and what it returns.
// без нотации
const foo = (a, b) => a + b
// в функции выше мы не можем понять, это функция по сложению или конкатенации
// конечно мы можем назвать правильно функцию foo
// конечно мы можем написать функцию вот так
const concat = (a, b) => a + b // конкатенация
const add = (a, b) => a + b // сложение
// с нотацией
// Название функции :: Первый аргумент -> Второй аргумент -> Возвращаемое значение
// add :: Number -> Number -> Number
const add = (a, b) => a + b // сложение
// map :: (a -> b) -> [a] -> [b]
const map = (mappable) => (arr) => arr.map(mappable)
// тоже самое но более классический подход в рамках теории категории
// и Спецификации Fantasy-land не пугаемся об этом поговорим позже
// Тип Функтор => Принимает функцию (a значение превращает в b) -> Функтор a аргумент -> Функтор b возвращает
// Спойлер )) функтор это любой тип, который реализовал интерфейс map
// Functor f => (a → b) → f a → f b
const map = (mappable) => (arr) => arr.map(mappable)07 Category theory
There is a close connection between functional programming and category theory. In functional programming many abstractions, laws and principles are taken from category theory.
So what do we know about category theory? We will not dive into the fundamentals and academic terms. We will try to explain all of this without academic terminology.
Category theory in the context of programming is a collection of abstract objects (any data types: Array, Object, Number, String, ...) and their morphisms (arrows, the relations between them — that is, functions), as well as the laws by which they operate (composition and so on).
// Пример
a -> b -> c
a, b, c // это объекты
-> // это их отношения и процессы над ними
a ... c // процесс композиции
// Практический пример
// concatX :: Number -> String
const concatX = num => `${num} X`
// toArray:: a -> [a]
const toArray = word => [word]
// compose :: (* -> *) -> (* -> *) -> * -> *
const compose = (f2, f1) => (...args) => f2(f1(...args))
const pipeline = compose(
toArray, // строку превращаем в массив
concatX // добавляем X и превращаем в строку
)
// start
pipeline(5) // ['5 X'] результат
// Что же тут произошло
// мы входной аргумент 5 это "a" тип Number
// конкатенировали букву X и получили '5 X' это "b" тип String
// добавили в массив и получили ['5 X'] это "с" тип Array
// и все это сделали законом композиции
// 5 -> '5 X' -> ['5 X'] это абстрактные объекты (Number, String, Array) их отношение объектов(вызовы функций -> ) по законом композиции
// a -> b -> cLet's use our imagination to understand category theory more deeply. We will draw analogies from the world around us.
We will use nature and write pseudocode that implements category theory.
// Пример
a -> b -> c
твердое -> жидкое -> газообразное
лёд -> вода -> пар
лёд, вода, пар // это объекты
-> // процессы над ними (нагрев и тп)
a ... c // процесс композиции (упорядоченная совокупность процессов)
// Практический пример
// превратитьВВоду :: лёд -> вода
const превратитьВВоду = лёд => (держим в комнатной температуре, вода)
// превратитьВПар:: вода -> пар
const превратитьВПар = вода=> (производим сильный нагрев, пар)
// композицияПроцессов :: (* -> *) -> (* -> *) -> * -> *
const композицияПроцессов = (f2, f1) => (...args) => f2(f1(...args))
const pipeline = композицияПроцессов(
превратитьВПар,
превратитьВВоду
)
// start
pipeline(лёд) // пар результат08 Fantasy-land
The Fantasy Land specification is a set of rules and public interfaces describing abstractions from category theory.
These rules and laws rest on algebraic data structures.
In essence, by implementing these interfaces and laws, we follow the Fantasy Land specification.
// Вот как она выглядит
Setoid Semigroupoid Semigroup Foldable Functor Contravariant Filterable
(equals) (compose) (concat) (reduce) (map) (contramap) (filter)
| | | \ / | | | | \
| | | \ / | | | | \
| | | \ / | | | | \
| | | \ / | | | | \
| | | \ / | | | | \
Ord Category Monoid Traversable | | | | \
(lte) (id) (empty) (traverse) / | | \ \
| / | | \ \
| / / \ \ \
| Profunctor / \ Bifunctor \
| (promap) / \ (bimap) \
| / \ \
Group / \ \
(invert) Alt Apply Extend
(alt) (ap) (extend)
/ / \ \
/ / \ \
/ / \ \
/ / \ \
/ / \ \
Plus Applicative Chain Comonad
(zero) (of) (chain) (extract)
\ / \ / \
\ / \ / \
\ / \ / \
\ / \ / \
\ / \ / \
Alternative Monad ChainRec
(chainRec)Let's try to create a Functor according to the Fantasy Land specification.
// Сигнатура
fantasy-land/map :: Functor f => f a ~> (a -> b) -> f b
// Создание функтора
// ==== синтаксис es5 ======
// подготавливаем прототип для функции конструктора
// удалив наследование Object.prototype дефолтная
var prototypeFunctor = Object.create(null)
// Название строки функции и метода согласно спецификации
const FANTASY_LAND_MAP = 'fantasy-land/map'
// создаем функцию конструктор
function Functor(value) {
this.value = value
}
// создаем правильные связи между прототипом и функций конструктором
Functor.prototype = prototypeFunctor
prototypeFunctor.constructor = Functor
// а вот теперь самое главное реализовываем интерфейс функтора
prototypeFunctor[FANTASY_LAND_MAP] = function (fn) {
return new Functor(fn(this.value))
}
// Давайте разделим ключевую логику на части
// return new Functor(fn(this.value))
// 1. Текущей содержимое контейнера мы пропускаем через функцию
fn(this.value)
// 2. Возвращаем новый экземпляр типа функтор уже с новым значением
return new Functor(тут результат инструкции выше)
// Пример
var functorObj = new Functor(5)
var functorResult = functorObj[FANTASY_LAND_MAP](value => value * 2)
// отсутствуют мутации
// реализован интерфейс спецификации
console.log(functorObj.value) // 5
console.log(functorResult.value) // 10
// =========================
// Полифил
// как устроен примерно внутри Object.create(objectProto)
var createObjectProto = function (obj) {
// для создания экземпляра формируем конструктор
function XXX() {}
// устанавливаем правильные ссылки
XXX.prototype = obj || Object.prototype
XXX.prototype.constructor = XXX
// создаем экземпляр и возвращаем
return new XXX()
}
// ==== синтаксис es6 ======
// тут все гораздо проще
// тут в прототипе мы также можем уничтожить дефолтное наследование Object.prototype
class Functor {
constructor (value) {
this.value = value
}
// этот метод и так уже будет хранится по дефолту в прототипе
[FANTASY_LAND_MAP] (fn) {
return new Functor(fn(this.value))
}
}
// Пример описанный выше также сработает
const functorObj = new Functor(5)
// =========================
// ==== через чистые функции ======
// Создание функтора без создания экземпляров
// через чистые функции
const functor = value => ({
value,
[FANTASY_LAND_MAP]: (fn) => functor(fn(value))
})
// создали объект и вернули его
const objA = functor(10)
const objB = objA[FANTASY_LAND_MAP](value => value - 5)
console.log(objA.value) // 10
console.log(objB.value) // 5
// =========================09 Function composition
Function composition is the ability of functions to be called in sequence, where the result of one function is passed to the next.
Function composition is one of the fundamental concepts on which category theory is based.
// === композиция из 3 функций ===
// compose :: (c -> z) -> (b -> c) -> (a -> b) -> a -> z
const compose = (f3, f2, f1) => (...args) => f3(f2(f1(...args)))
// использование функции
// наша цель
// 1. Получить пользователей со статусом онлайн
// 2. Их имена
// 3. Вывезти список имен в лог
// fetchAllUsers :: * -> Promise a
const fetchAllUsers = () => new Promise(resolve => resolve([
{
name: 'John',
status: 'online',
age: 25
},
{
name: 'Alex',
status: 'offline',
age: 18
},
{
name: 'Angel',
status: 'online',
age: 18
}
]))
fetchAllUsers() // имитируем запрос
.then( // дожидаемся ответа
compose( // результат бросаем в композицию функций
console.log, // логируем результаты
(usersActive) => usersActive.map(user => user.name), /// собираем имена
(data) => data.filter(user => user.status === 'online'), // фильтруем по статусу
)
)
// таже самая композиция только в рамках типа Array
// не совсем классический функциональный подход
fetchAllUsers() // имитируем запрос
.then( // дожидаемся ответа
(data) => console.log(
data
.filter(user => user.status === 'online')
.map(user => user.name)
)
)
// ===============================If you noticed in the example above, the functions executed from right to left — mathematically that's fine. But it is often more convenient for the reader to read code from left to right.
So we can rewrite the code above.
// === композиция из 3 функций в человеко читаемом формате ===
// порядок сменился на слева на право
// pipe :: (a -> b) -> (b -> c) -> (c -> z) -> a -> z
const pipe = (f1, f2, f3) => (...args) => f3(f2(f1(...args)))
// Теперь композицию удобнее читать слева на право
fetchAllUsers() // имитируем запрос
.then( // дожидаемся ответа
pipe( // результат бросаем в композицию функций
(data) => data.filter(user => user.status === 'online'), // фильтруем по статусу
(usersActive) => usersActive.map(user => user.name), /// собираем имена
console.log, // логируем результаты
)
)
// Внимание
// compose порядок выполнения функций
3 <- 2 <- 1
// pipe
1 -> 2 -> 3
// ===============================Now imagine a situation where the number of functions is more than 3.
In real projects, use compose or pipe from existing libraries. We will try to build compose in several ways: (imperatively) (declaratively) (recursively).
// === композиция функций в нестрого заданном количестве функций ===
// === Способ императивный ===
// порядок сменился на слева на право
// pipe :: (a -> b) -> ..., -> a -> z
const pipe = (...fns) => (...args) => {
// дробим массив функций на голову и хвост ))
const [ head, ...tail ] = fns
// переменная с результатом выполнения
// первой и остальных функций и результата
let result = head(...args)
// в цикле вызываем остальные функции и перезаписываем результат
// в переменную
for (const fn of tail) {
result = fn(result)
}
// возвращаем итоговый результат
return result
}
// ===============================Now let's implement it in a functional style, declaratively.
// === композиция функций в нестрого заданном количестве функций ===
// === Способ декларативный ===
// порядок сменился на слева на право
// pipe :: (a -> b) -> ..., -> a -> z
const pipe = (...fns) => (arg) => fns.reduce(
(init, fn) => fn(init), // акумм значение или инит бросаем в функцию
arg // аргумент первого вызова
)
// на первый взгляд в этой функции все топчик))
// и с точки зрения функционального подхода аргумент лучше и правильнее когда один
// но может быть ситуация когда при первом запуске аргументов 2 и более
const pipe = (...fns) => (...args) => fns.reduce(
(init, fn, i) => i === 0 ? fn(...init) : fn(init) , // проверка первый вызов или остальные
args // аргументы первого вызова
)
// ===============================We can also implement function composition recursively. We'll apply tail-call optimization right away and cut the number of calls in half.
// === композиция функций в нестрого заданном количестве функций ===
// === Способ рекурсивный ===
// порядок сменился на слева на право
// pipe :: (a -> b) -> ..., -> a -> z
const pipe = (...fns) => (arg) => {
// базовый случай — одна функция осталась
if (fns.length === 1) {
return fns[0](arg)
}
// разбиваем голову и хвост
// первая функция и остальные
const [head, ...tail] = fns
// рекурсивно вызываем pipe для хвоста
// а первую функцию вызываем с аргументом текущим
return pipe(...tail)(head(arg))
}
// ===============================10 Currying
Currying is one of the techniques used in functional programming, in which the function is evaluated only once all of its arguments have been supplied, while given only some of the arguments the function returns a new function.
Currying can be used:
1. In function composition, when a function in a chain of calls expects two or more arguments.
2. When we want to create specialized functions based on a base function by changing the value of an argument captured in a closure.
// Пример
// Каррирование
// sum :: Number -> Number -> Number -> Number
const sum = (a) => (b) => (c) => a + b + c
// функция вычисляется только когда получает все аргументы
// иначе возвращает новая функция
const result = sum (1) (2) (1)
console.log(result) // 4
// Неполные вызовы каррированной функции
// add : Number -> Number -> Number
const add = (a) => (b) => a + b
// в замыкании хранится 10 и ждёт второго аргумента
// возвращается функция
const add10 = add(10)
// после получения второго аргумента
// происходит полное вычисление функции
const resultAdded = add10(5) // 15So how do we curry an arbitrary function with n parameters whose values we don't know — for example, a function with 5 or more parameters?
We can apply automatic currying to any function and call it in any order.
// Например мы хотим каррировать функцию sum
// sum :: Number -> Number -> Number -> Number
const sum = (a, b, c) => a + b + c
// функция которая каррирует любую функцию
// curry :: (* -> *) -> * -> (* -> *)
const curry = (fn) => {
// количество параметров функции
const lengthFn = fn.length
// внутренняя функция, которая будет вызываться рекурсивно
const curried = (...argsFirst) => {
// если количество входящих аргументов больше или равно
// количество аргументов функции то мы немедленно вызываем её
if (argsFirst.length >= lengthFn) {
return fn(...argsFirst)
}
// если входящих аргументов меньше, чем количество
// аргументов у функции то возвращаем новую функцию
// и собираем остальные аргументы
return (...argsSecond) => curried(...argsFirst, ...argsSecond)
}
return curried
}
// каррируем нашу функцию sum
const sumCurry = curry(sum)
// теперь можем вызывать ее как угодно
console.log( sumCurry (1) (1) (1) ) // 3
console.log( sumCurry (1, 1) (1) ) // 3
console.log( sumCurry (1) (1, 1) ) // 3
console.log( sumCurry (1, 1, 1) ) // 3Now let's look at the full power of currying through examples of function composition.
// Например в следующей композиции будут
// =====================================
// использоваться две функции каррированые
// одна каррированая уже
// а другая автоматически каррируем перед вызовом
// данные с которыми будем работать
// список пользователей
// fetchAllUsers :: * -> Promise a
const fetchAllUsers = () => new Promise(resolve => resolve([
{ name: 'John', status: 'online', age: 28 },
{ name: 'Angel', status: 'online', age: 35 },
{ name: 'Michel', status: 'offline', age: 18 },
{ name: 'Anna', status: 'offline', age: 20 },
]))
// Наша задача
// 1. Получить пользователей со статусом онлайн
// 2. Возрастом старше 30 лет
// 3. их имена
// filter :: (a -> Boolean) -> [a] -> [a]
const filter = (fn) => (arr) => arr.filter(fn)
// map :: (a -> b) -> [a] -> [b]
const map = (fn) => (arr) => arr.map(fn)
// функция предикат, которая будет проверять статус
// isStatus :: String -> String -> a -> Boolean
const isStatus = (status) => (prop) => (user) => user[prop] === status
// isMoreAge :: String -> String -> a -> Boolean
const isMoreAge = (age, prop, user) => user[prop] > age
// давайте используя функцию выше мы автокаррируем её
const isMoreAgeCurry = curry(isMoreAge)
// вызовим дважды и создадим абстракцию с понятным названием
// предикат функция которая будет проверять на возраст
const isMoreAge30 = isMoreAgeCurry (30) ('age')
// получение значений по ключу
// getByProp :: String -> a -> String
const getByProp = (prop) => (user) => user[prop]
// в замыкании сохраняем ключ 'name' и ожидаем объект
// getName :: a -> String
const getName = getByProp('name')
// готовим композицию и при след вызове в качестве аргумента
// ждем массив пользователей
// представим что pipe функция уже реализована или готова
const pipeline = pipe (
filter ( isStatus ('online') ('status') ), // только онлайн пользователей получаем
filter ( isMoreAge30 ), // возрастом старше 30 лет
map ( getName ), // вернуть имена
)
(async () => {
// запуск промиса с данными
// данные бросаем в композицию и запускаем её
const adultsUsers = await fetchAllUsers()
.then(pipeline)
console.log(adultsUsers) // ['Angel']
})
// =============================
// в коде выше все круто описано
// но если пользователей несколько десятков тысяч
// и нам нужна оптимизация мы можем сделать так
// чтобы исключить двойной обход массива в рамках filter
const pipeline = pipe (
filter (
(user) => (
isStatus ('online') ('status') (user) && isMoreAge30 (user)
)
), // разовый проход с проверкой по двум предикатам сразу
map ( getName ), // вернуть имена
)We can also create specialized functions with the help of currying. It is somewhat similar to "inheritance" in OOP.
We take a base curried function, and then we build other functions through calls. Enough talking)) let's get straight to the examples.
// Давайте возьмем каррированную функцию
// и начнем создавать другие специализированные функции
// Базовая функция сравнения значения по определенному ключу
// equalBy :: String -> * -> a -> Boolean
const equalBy = (prop) => (value) => (obj) => obj[prop] === value
// данные с которыми мы будем работать
const user = { name: 'John', status: 'online', married: true }
// мы бы могли вызвать функцию разом
const onlineStatusUser = equalBy ('status') ('online') (user)
// проверяем онлайн ли статус у пользователя
console.log(onlineStatusUser) // true
// теперь давайте начнем создавать
// специализированные функции из equalBy
// сравнение по ключу статусу
const equalByStatus = equalBy ('status')
// сравнение по ключу женат
const equalByMarried = equalBy ('married')
// создадим еще один слой абстракции
// проверка онлайн
const isOnline = equalByStatus ('online')
// проверка оффлайн
const isOffline = equalByStatus ('offline')
// проверка женат ли
const isMarried = equalByMarried (true)
// проверка не женат
const isNotMarried = equalByMarried (false)
// мы можем написать хелпер отрицания
// для последних функций
// not :: (a -> Boolean) -> a -> Boolean
const not = (fn) => (...args) => !fn(...args)
// человеко читаемое выражение не онлайн
const offlineUserCheck = not (isOnline)
const notMarriedCheck = not (isMarried)
// а теперь давайте запустим наши спец функции
// и проверим онлайн ли пользователь и женат ли он
// получим статус юзера
const isOnlineUser = isOnline (user)
// получим его статус женат ли
const isMarriedUser = isMarried (user)
// теперь давайте допускать в наше приложение
// пользователей женатых и которые онлайн
// напишем хелпер
// and :: Boolean -> Boolean -> Boolean
const and = (valueFirst) => (valueSecond) => valueFirst && valueSecond
// обратите внимание код стал человекочитаемым
// и очень абстрактным
// каждая функция делает одну логическую операцию
// код говорит что делает а не как
if (and (isOnlineUser) (isMarriedUser) ) {
// что то там делам ...
}11 Currying and asynchrony (evolution)
Currying can be used effectively in asynchronous programming.
1. Creating specialized functions
2. We will defeat "callback hell" without promises
// Давайте создадим базовую функцию для запросов
// baseFetch :: String -> String -> a -> Promise
const baseFetch = (baseUrl)
=> (params)
=> (config = {}) =>
fetch(`${baseUrl}/${params}`, config)
const BASE_URL = 'https://example.com'
// для работы с базовым url
const useFetch = baseFetch (BASE_URL)
// создадим две специализированные функции
// получение списка всех пользователей
const getAllUser = useFetch ('all-users')
// получения списка всех книг
const getAllBooks = useFetch ('all-books')
// дальше можем вызывать функции и получать данные
( async () => console.log( await getAllUser () )) ()
( async () => console.log( await getAllBooks () )) ()
// вы можете добавить авторизационные токены
// в отдельном замыкании если это необходимо
// xFetch :: String -> b -> String -> a -> Promise
const xFetch = (baseUrl)
=> (headers)
=> (params)
=> (config = {}) =>
fetch(`${baseUrl}/${params}`, {
...headers,
...config,
})
const token = 'xxx'
// для работы с базовым url и установка заголовком сразу
const useXFetch = xFetch (BASE_URL) ({
headers: {
'Content-Type': 'application/json',
'authorization': `Bearer ${token}`,
}
})
// создадим две специализированные функции
// получение списка всех пользователей
const getAllUserX = useXFetch ('all-users')
// получения списка всех книг
const getAllBooksX = useXFetch ('all-books')Now below we will show two examples.
The first piece of code will be imperative and callback-based, while the second will use currying and closures without promises.
// код который "плохо пахнет"
// создадим функцию для запросов на основе XMLHttpRequest
// он на событиях без промисов
// можно конечно обернуть в промисы, но сейчас у нас тема каррирование))
const fetchXML = (url, callback) => {
const req = new XMLHttpRequest()
req.addEventListener('load', () => {
const data = JSON.parse(req.responseText)
callback(data)
})
req.open('GET', url)
req.send()
}
// Чего мы хотим
// 1. При клике на кнопку подробнее
// у определенного пользователя мы получим его слаг из дата атрибута
// 2. По слагу сделаем запрос и получим список отзывов,
// 3. Берем слаг последнего отзыва (первый в списке) и делаем запрос
// 4. Получаем подробную информацию об отзыве, берем оттуда текст
// текс который покажем
var textReview = ''
// используем делегирование на всем контейнере с кнопками
document.getElementById('users-wrapper')
.addEventListener('click', (e) => {
// получаем элемент
const el = e.target
// если кнопка не с пользователем но игнорим клик
if(!el.classList.contains('user-btn')) {
return
}
// получаем слаг пользователя с его кнопки
const slugUser = el.dataset.slug // 'xksdfjdkhfd' это слаг
// проверка на слаг
if(!slugUser) {
return
}
// получаем список отзывов по слагу пользователя
fetchXML('https://example.com/reviews/' + slugUser, (data) => {
// получаем список пользователей
const reviews = data.reviews
// получаем слаг последнего отзыва
const slugFirstReview = reviews[0].slug
// делаем запрос и получаем подробную информацию по отзыву
fetchXML('https://example.com/review/' + slugFirstReview, (data) => {
const review = data.review
textReview = review
})
})
})We will use currying without promises and defeat "callback hell".
Let's refactor our code.
// декларативный код
// ==== ВНИМАНИЕ многие функции хелперы уже
// ==== существуют в готовых библиотеках
// ==== тут мы это часто показываем в учебных целях
// создадим функцию для запросов на основе XMLHttpRequest
// он на событиях без промисов
// можно конечно обернуть в промисы, но сейчас у нас тема каррирование))
// fetchXML :: String -> String -> String -> (a -> b) -> c
const fetchXML = (baseUrl) => (params) => (id) => (callback) => {
const req = new XMLHttpRequest()
req.addEventListener('load', () => callback(JSON.parse(req.responseText)))
req.open('GET', `${baseUrl}/${params}/${id}`)
req.send()
return req
}
// pipe :: ((a -> b), (b -> c), ..., (y -> z)) -> a -> z
const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x)
// давайте на основе функции выше создадим спец функции
const BASE_URL = 'https://example.com'
const REVIEWS = 'reviews'
const REVIEW_INFO = 'review-info'
// функция закрепляющая базовый путь
const fetchBaseUrl = fetchXML(BASE_URL)
// функция для получения списка отзывов
const getReviews = fetchBaseUrl(REVIEWS)
// функция для получения подробностей по опред отзыву
const getReviewInfo = fetchBaseUrl(REVIEW_INFO)
// теперь создадим хелпер для сообщения клик
// чтобы вызывать в композиции функций
// clickElement :: a -> (a -> *) -> *
const clickElement = (element) =>
(callback) =>
element.addEventListener('click', callback)
// Чего мы хотим
// 1. При клике на кнопку подробнее
// у определенного пользователя мы получим его слаг из дата атрибута
// 2. По слагу сделаем запрос и получим список отзывов,
// 3. Берем слаг последнего отзыва (первый в списке) и делаем запрос
// 4. Получаем подробную информацию об отзыве, берем оттуда текст
// head :: [a] -> a
const head = (arr) => arr[0]
// prop :: String -> a -> b
const prop = (prop) => (obj) => obj[prop]
// текс который покажем
var textReview = ''
// получаем контейнер с кнопками
const btnWrapper = document.getElementById('users-wrapper')
// запускаем сразу композицию
clickElement (btnWrapper) (pipe(
(e) => e?.target, // получаем элемент
(el) => el?.dataset?.slug, // получаем дата атрибут
(slugUser) => getReviews (slugUser) ( // делаем запрос на получение всех отзывов
pipe(
head, // берем первый отзыв
prop('slug'), // оттуда слаг
(slug) => getReviewInfo (slug) ( // по слагу делаем запрос на получение инфы
(review) => {
textReview = prop ('text') (review) // результат записываем в глобальную переменную
})
)
)
))I'll admit to you ))) the smell of bad code hasn't gone away.
What if we add the power of promises to the code above and transform it.
// А теперь возьмемся за дело
// перепишем нашу функцию получения данных
// которая сама возвращает промис
// fetchClassic :: String -> String -> String -> Promise
const fetchClassic = (baseUrl) =>
(params) =>
(id) =>
fetch (`${baseUrl}/${params}/${id}`)
.then (res => res.json())
const BASE_URL = 'https://example.com'
const REVIEWS = 'reviews'
const REVIEW_INFO = 'review-info'
// получения списка отзывов
const getReviews = fetchClassic (BASE_URL) (REVIEWS)
const getReviewInfo = fetchClassic (BASE_URL) (REVIEW_INFO)
// У нас будет супер пайп, который под капотом
// будет проверять промисы ))
// pipe :: ((a -> b), (b -> c), ..., (y -> z)) -> a -> z
const pipeAsync = (...fns) => (x) => fns.reduce((v, f) => {
return v instanceof Promise
? v.then(f)
: f(v)
}, x)
// запускаем улучшенный вариант
// pipeAsync отрабатывает промисы под капотом
clickElement (btnWrapper) (pipeAsync(
(e) => e?.target, // получаем элемент
(el) => el?.dataset?.slug, // получаем дата атрибут
getReviews, // делаем запрос на получение всех отзывов
head, // берем первый отзыв
prop('slug'), // оттуда слаг
getReviewInfo, // по слагу делаем запрос на получение инфы отзыва
prop('text'), // берем поле text
(text) => textReview = text, // устанавливаем значением в глоб переменную
))12 Partial application
Partial application is one of the techniques used in functional programming, in which the function is evaluated only once all of its arguments have been supplied.
Partial application is very similar to currying, but there are differences.
1. With partial application the number of deferred calls is usually no more than two.
2. The number of arguments passed to the function in partial application can be several, whereas in classic currying it is usually one.
// пример функции с частичным применением
// partialSumA :: Number -> Number -> Number -> Number
const partialSumA = (a) => (b, c) => a + b + c
// partialSumB :: Number -> Number -> Number -> Number
const partialSumB = (a, b) => (c) => a + b + c
// давайте теперь вызовем эти функции
console.log ( partialSumA (1) (1, 1) ) // 3
console.log ( partialSumB (1, 1) (1) ) // 3
// А давайте напишем функцию, которая применяет
// частичное применение к любой обычной функции
// ((a, b, c, …, n) → x) → [a, b, c, …] → ((d, e, f, …, n) → x)
// partial:: (a -> b) -> (a ->b)
const partial = (fn) => {
// количество аргументов у вызываемой функции
const lengthArgs = fn.length
// возвращаемая функция
const innerPartial = (...firstArgs) => {
// если количество входящих аргументов
// больше или равно чем у функции вызывающей
// значит сразу можно ее вызвать
if (firstArgs.length >= lengthArgs) {
return fn (...firstArgs)
}
// иначе мы вторым вызовом
// запускаем функцию в любом случае
return (...secondArgs) => fn (...firstArgs, ...secondArgs)
}
return innerPartial
}
// можем применить нашу функцию
// которая применяет частичное применение
// к любой функции
// sum :: Number -> Number -> Number -> Number
const sum = (a, b, c) => a + b + c
// возвращаем функцию с супер способностями )
const sumPartial = partial (sum)
// теперь мы можем вызывать возвращенную функцию
console.log ( sumPartial (1) (1, 1) ) // 3
console.log ( sumPartial (1, 1) (1) ) // 313 Recursion
Recursion is the ability of a function to call itself.
Recursion must meet two basic requirements in order to work correctly.
1. The base case. This is the logic at which the recursion must stop — the base condition that stops the function from calling itself.
2. The recursive case. This is the logic in which the function calls itself, usually with small changes to the arguments.
Recursion is frequently used in functional programming.
// наша цель получить средний возраст пользователей
// ================================
// декларативный стиль без рекурсии
// список пользователей
const fetchAllUsers = () => new Promise(resolve => resolve([
{ name: 'John', status: 'online', age: 28 },
{ name: 'Angel', status: 'online', age: 35 },
{ name: 'Michel', status: 'offline', age: 18 },
{ name: 'Anna', status: 'offline', age: 20 },
]))
// getAgeAverage :: [a] -> Number
const getAgeAverage = (users) => pipe (
(users) => [
users.reduce ((init, { age }) => init + age, 0),
users.length
], // получаем общий возраст и количество пользователей
([allAges, quantity]) => Math.round (allAges / quantity), // вычисляем средний возраст
console.log // логируем результат
) (users)
fetchAllUsers().then(getAgeAverage)
// ================================
// Рекурсивный пример
// ================================
// эта функций рекурсивная оптимизирована
// она не делает 2 * вызовов
// потому что по достижению базового случая она
// сразу возвращает результат
// но про это мы поговорим еще позже
// а теперь тоже самое сделаем с помощью рекурсии
// recurAgeAverage :: [a] -> Number -> Number -> Number
const recurAgeAverage = (
users, // массив данных пользователей
sumAges = 0, // сумма возрастов пользователей
quantityUsers = users.length // первичное количество пользователей
) => {
// базовый случай, условие выхода
// когда массив уже пуст
if (!users.length) {
// сумму всех лет пользователей делим на количество пользователей
// чтобы посчитать средний возраст пользователей
return sumAges / quantityUsers
}
// рекурсивный случай, чтобы вызывать самого себя
// пока в массиве есть элементы
return recurAgeAverage (
users.slice(1), // возвращаем массив без первого элемента
sumAges = sumAges + users[0]?.age, // собираем возраст каждого пользователя
quantityUsers // передаем ранее сохраненное значение количество пользователей
)
}
// ================================
// Давайте нашу функцию перепишем в одну строку и вызовем
const recurAgeAverageF = (users, sumAges = 0, quantityUsers)
=> !users.length
? sumAges / quantityUsers
: recurAgeAverageF (
users.slice(1),
sumAges = sumAges + users[0]?.age,
quantityUsers
)
( async () => {
const users = await fetchAllUsers () // получаем список пользователей
const averageAge = recurAgeAverageF (users) // получаем средний возраст рекурсивно
console.log (averageAge) // логируем результат 25.25
}
)()The example above with recursion may not be simple for some. So we will write a simpler example and visualize the calls. The main thing is, don't worry if something isn't clear right away.
// Наша цель собрать общий рейтинг пользователей
// То есть сумму рейтингов
const users = [
{ name: 'John', rating: 4.5 },
{ name: 'Angel', rating: 4 },
{ name: 'Michel', rating: 3 },
{ name: 'Anna', rating: 5 },
]
// sumRating : [a] -> Number
const sumRating = (users) => {
// базовый случай, условие выхода когда массив пуст
if (!users.length) {
return 0
}
// рекурсивный случай
// рейтинг первого элемента складывается
// с вызовом самой же функции с вырезанным эти элементом
return users[0].rating + sumRating (users.slice(1))
}
// давайте сократим синтасис
const rateSum = (users) =>
!users.length
? 0
: users[0] + rateSum (users.slice(1))
// визуализируем вызовы
// и для понимания представим массив числе вместо ключа rating
rateSum ([4.5, 4, 3, 5]) // возвращает 4.5 + rateSum ([4, 3, 5])
rateSum ([4, 3, 5]) // возвращает 4 + rateSum ([3, 5])
rateSum ([3, 5]) // возвращает 3 + rateSum ([5])
rateSum ([5]) // возвращает 5 + rateSum ([])
rateSum ([]) // возвращает 0 достигли базового случая
// и тут самое главное идут возвраты от вызовов в стэке
rateSum ([5]) // возвращает 5 + 0
rateSum ([3, 5]) // возвращает 3 + 5
rateSum ([4, 3, 5]) // возвращает 4 + 8
rateSum ([4.5, 4, 3, 5]) // возвращает 4.5 + 12
// итоговый результат 16.5The example above with the "rateSum" function has one significant drawback — the number of calls, which can be optimized using the tail call optimization technique.
Why do we need this? In JS the call stack overflows during recursive synchronous calls, because functions are not created lazily — that is, they are created before the moment of the call, unlike in Haskell, where there are no loops and functions are created lazily at the moment of the actual call.
// === Оптимизация хвостовой рекурсии ====
// Давайте избавимся от лишних вызовов
const rateSum = (users, result = 0) => {
// базовый случай условие выхода когда массив пуст
// возвращаем второй аргумент где аккумулируем
// результаты всех вызовов
if (!users.length) {
return result
}
// рекурсивный случай пока в массиве элементы
// при каждом вызове результат складываем во второй аргумент
// и вызываем дальше и при базовой случай возвращаем результат
// тем самым сокращаем количество вызовов
return rateSum (users.slice(1), result = result + users[0])
}
// давайте все перепишем в одну строку
const sum = (users, result = 0) => !users.length
? result
: sum (users.slice(1), result = result + users[0])
// визуализируем вызовы
// и для понимания представим массив числе вместо ключа rating
sum ([4.5, 4, 3, 5], 0) // возвращает sum ([4, 3, 5], 0 + 4.5)
sum ([4, 3, 5], 4.5) // возвращает sum ([3, 5], 4.5 + 4)
sum ([3, 5], 8.5) // возвращает sum ([5], 8.5 + 3)
sum ([5], 11.5) // возвращает sum ([], 11.5 + 5)
sum ([], 16.5) // возвращает result 16.5
// дальше вызовы прекращаются
// удержания и возвратов нет
// размотки стека нет
// возвращаем итоги
// количество вызовов сократилось вдвоеTail call optimization, as of now (the year 2025), is unfortunately not implemented in the Node.js (Chrome) engine. It was implemented in one of the versions of Core JS (Safari), but was later removed. In any case the number of calls is reduced. In production code, try not to use it when you don't have precise information about the number of calls in the program.
Further on in the recursion section we will use techniques that let you safely use recursion in production code without counting the number of calls.
So how do we free up the call stack without overflowing it in production code.
1. Make the code asynchronous, avoid blocking the thread, and free up the stack
2. Use a thunk — a "trampoline", a fake recursion
// === 1. Асинхронный вызов рекурсии ====
// обратите внимание, что рекурсивный случай оборачивается в таймер
// sum :: [a] -> Number -> (Number -> *)
const sumD = (users, result = 0, callback) => {
// базовый случай условие выхода
// когда массив уже пустой то результат пробрасываем в колбек
if (!users.length) {
callback (result)
return
}
// рекурсивный случай вызываем асинхронно
// через таймер чтобы стек не переполнялся
setTimeout (
() => sumD (users.slice(1), result = result + users[0], callback),
0
)
}
// давайте сократим запись
const sum = (users, result = 0, callback) =>
!users.length
? callback (result)
: setTimeout (
() => sum (users.slice(1), result = result + users[0], callback),
0
)
// попробуем вызвать
sum ([4.5, 4, 3, 5], 0, (total) => console.log(total)) // 16.5
// визуализируем вызовы
sum ([4.5, 4, 3, 5], 0, (total) => console.log(total)) // возвращает sum ([4, 3, 5], 0 + 4.5, callback)
// стэк вызовов свободный для микрозадач (промисы) или макро например
// отсутствует блокировка потока
// отсутствует переполнение стека
sum ([4, 3, 5], 4.5, (total) => console.log(total)) // возвращает sum ([3, 5], 4.5 + 4, callback)
// стэк вызовов свободный для микрозадач (промисы) или макро например
// отсутствует блокировка потока
// отсутствует переполнение стека
sum ([3, 5], 8.5, (total) => console.log(total)) // возвращает sum ([5], 8.5 + 3, callback)
// стэк вызовов свободный для микрозадач (промисы) или макро например
// отсутствует блокировка потока
// отсутствует переполнение стека
sum ([5], 11.5, (total) => console.log(total)) // возвращает sum ([], 11.5 + 5, callback)
// стэк вызовов свободный для микрозадач (промисы) или макро например
// отсутствует блокировка потока
// отсутствует переполнение стека
sum ([], 16.5, (total) => console.log(total)) // вызовет callback c result
// Минусы этого подхода
// Скорость выполнения (производительность) низкая
// Если вам необходимы длительные операции вызовы функций синхронные
// То лучше использовать веб воркеры в отдельном потоке
// При условии, что у вас процессор больше одного ядраLet's examine the logic of using a trampoline to avoid stack overflow. The trampoline works synchronously, unlike a call through a timer.
Let's look at the main principles of the trampoline.
1. A trampoline is a wrapper function that gives our main function special behavior.
2. The trampoline function returns another function that accepts arguments.
3. Inside, it calls the main function with an argument in a loop.
4. If the returned value is a function, it will call it again; otherwise it stops the loop and returns the result.
5. In the base case the main function must return a result.
6. In the recursive case the main function must necessarily return a function.
// === Рекурсия через трамплин ====
// обратите внимание первый аргумент это функция
// а вторым аргументом мы по сути запускаем вызов функции
// trampoline :: ((...args -> *) -> (...args -> *))
const trampoline = (fn) => (...args) => {
// запускаем функцию с первым аргументом
let result = fn(...args)
// запускаем цикл бесконечно пока функция
// возвращает другую функцию
while (typeof result == 'function') {
// результат вызова сохраняем в переменную
// и проверяем что оно вернуло
// result() *** запомни эту метку дальше покажем что за функция это
result = result()
}
// как только результат вызова функции
// не функция то возвращаем его
return result
}
// давайте сократим запись
// sumD :: [a] -> Number -> Number
const sumD = (users, result = 0) => {
// если массив пустой то возвращаем результат
if (!users.length) {
return result
}
// внимание тут обязательно возвращаем функцию
// метка *** эта функция запускаемая в трамплине внутри
return () => sumD (users.slice(1), result = result + users[0])
}
// давайте перепишем в одну строку функцию
// sum :: [a] -> Number -> Number
const sum = (users, result = 0) =>
!users.length
? result
: () => sum (users.slice(1), result = result + users[0])
// протестируем код
// пропускаем нашу функцию через трамплин
const sumTrampoline = trampoline (sum)
// теперь на самом деле в цикле будут запускаться функции
console.log (sumTrampoline ([4.5, 4, 3, 5], 0)) // 16.5
// давайте теперь визуализируем вызовы
// логика внутри функции трамплин
// === первый вызов =====
let result = sum([4.5, 4, 3, 5], 0) //
// вернет в result () => sum ([4, 3, 5], 4.5 + 0)
// =======================
// === обычные вызовы =====
while (typeof result == 'function') {
result = result() // () => sum ([4, 3, 5], 4.5 + 0)
// вернет в result () => sum ([3, 5], 4.5 + 4)
}
// =======================
// === обычные вызовы =====
while (typeof result == 'function') {
result = result() // () => sum ([3, 5], 4.5 + 4)
// вернет в result () => sum ([5], 8.5 + 3)
}
// =======================
// === обычные вызовы =====
while (typeof result == 'function') {
result = result() // () => sum ([5], 8.5 + 3)
// вернет в result () => sum ([], 11.5 + 5)
}
// =======================
// === последний вызов условие выхода из цикла =====
return result // где result это второй аргумент 16.5
// =======================
// визуализация через схему
// это как тип связный список функций, который заранее не готов
// а формируется в процессе
[(a -> b)] -> [(b -> c)] -> [(c -> d)]
// массив функция где след элемент формируется динамически
[
([4.5, 4, 3, 5]) => sum ([4.5, 4, 3, 5], 0), // первый вызов let result = fn(...args)
() => sum ([4.5, 4, 3, 5], 0), // result = result() внутри цикла
() => sum ([4, 3, 5], 4.5), // result = result() внутри цикла
() => sum ([3, 5], 8.5), // result = result() внутри цикла
() => sum ([5], 11.5), // result = result() внутри цикла
() => sum ([], 16.5), // result = result() внутри цикла
16.5 // цикл завершается return result
]14 Performance
It is worth noting that in typical cases imperative code runs slightly faster, but the difference is usually negligible. Sometimes functional code can even show the same or better results thanks to optimizations in the JavaScript engine. If maintainability, reliability, and a sound architecture are on one side of the scales, then functional code is undoubtedly the better choice.
Improving the speed of functional code is primarily the responsibility of the developers of JavaScript engines. That is, engine developers carry out improvements and optimizations of the compiler and the garbage collector.
Every abstraction introduces a small additional resource cost, which is often not critical.
In projects where speed is critical and the number of elements reaches several tens of thousands or even hundreds of thousands during synchronous execution, and we cannot spawn a separate thread due to a lack of hardware resources — namely a free CPU core — we can resort to low-level imperative tools.
In 98% of cases the performance of your application is determined more by the correct logic of how the program is built than by an extra abstraction.
// Произведем небольшие замеры
// ===============================
// функция, которая генерирует массив чисел от 1 до числа n включительно
// createArray :: Number -> [a]
const createNumberArray = (n) => new Array(n).fill(1).map((_, i) => i + 1)
// функция которая декларативно складывает числа в массиве
// sumD :: [a] -> Number
const sumD = (arr) => arr.reduce((acc, curr) => acc + curr)
// функция которая императивно складывает числа в массиве
const sumN = (arr) => {
let result = 0
const length = arr.length
for (let i = 0; i < length; i += 1) {
result = result + arr[i]
}
return result
}
// ===============================
// начнем тестировать
// ===============================
// создаем данные массив из 100 000 чисел
const dataNumber = createNumberArray (100000)
// замер времени декларативного кода
// ===============================
const startT = performance.now() // стартовое время
const resultD = sumD (dataNumber) // выполняемый код
const endT = performance.now() // конечное время
// вычисление дельты и логирования
console.log('Время выполнения декларативного кода ' + (endT - startT) + 'миллисекунд. Результат: ' + resultD)
// время выполнения конечно же зависит от вашего устройства и прогрева железа
// это тестирование не особо точное
// тут главное нам уловить разницу в императивном и декларативном коде
// 2.4274640000000005миллисекунд. Результат: 5000050000
// ===============================
// только подряд сразу не тестируйте )))
// замер времени императивного кода
// ===============================
const startI = performance.now() // стартовое время
const resultI = sumN (dataNumber) // выполняемый код
const endI = performance.now() // конечное время
// вычисление дельты и логирования
console.log('Время выполнения императивного кода ' + (endI - startI) + 'миллисекунд. Результат: ' + resultI)
// время выполнения конечно же зависит от вашего устройства и прогрева железа
// тут главное нам уловить разницу в императивном и декларативном коде
// 2.2674640000000005миллисекунд. Результат: 5000050000
// 2.380783000000008миллисекунд. Результат: 5000050000
// 2.600783000000008миллисекунд. Результат: 5000050000
// ===============================
// Неожиданно даже reduce код работает быстрее ХАХА
// Это нам говорит о том создатели движков работают над этим
// Даже если мы перепишем sumN через цикл while
// оно будет иметь почти такую же скорость
// reduce уже по сути нативное решение ))
// Это не противоречие моим словам выше это просто нативное улучшение
// Наши абстракции будут чуть больше использоватьFrom the above we can conclude that native solutions are more efficient.
Let's write a small utility for measurements and run some execution-speed tests.
// Утилита замера скорости
// ===============================
const benchmark = (fn) => (n = 100) => (msg = '') => {
// итоговый средний результат
let total = 0
// количество итераций вызова нашей функции
for (let i = 0; i < n; i++) {
// стартовый замер перед началом вызова функции
const start = performance.now()
// вызов функции
fn()
// аккумулируем результат дельты конечного и стартового результата
total += performance.now() - start
}
console.log(msg) // текст лога
// далее итоговое время делим на количество вызовов
// функции и выводим среднее арифметическое
return total / n
}
// создаем две функции
// для работы с массивом чисел
// подготовка данных
// ======================
// массив чисел 1 - 10 000
const dataNumbers = createNumberArray (100000)
// обертка для императивной функция
// и декларативной функции
// sumImperative :: ([a] -> Number) -> [a] -> (* -> Number) -> Number
const sumWrap = (fn) => (arr) => () => fn(arr)
// императивная функция складывания
const sumImperative = sumWrap (sumN) (dataNumbers)
// декларативная функция складывания
const sumDeclarative = sumWrap (sumD) (dataNumbers)
// Внимание одновременно обе функции сразу не прогоняйте
// в начале запустите одну, а потом через какое то время вторую
// замер императивной функции
const resultImperative = benchmark (sumImperative) (1000) ('замер завершён')
console.log (resultImperative) // 0.15482687300000053
// замер декларативной функции
const resultDeclarative = benchmark (sumDeclarative) (1000) ('замер завершён')
console.log (resultDeclarative) // 1.2971963320000012
// вот тут мы и выяснили, что все таки императивный код быстрее )))15 Neural networks and category theory
Let's try to look at how neural networks work through the lens of category theory. To do that, we will first recap the notion of category theory that we discussed in previous chapters.
Category theory is a collection of abstract objects (any objects) and their morphisms (the arrows of their relationships), as well as the laws by which they operate.
Category theory applied to neural networks is a collection of abstract objects (neurons or layers of neurons) and their morphisms (arrows, that is, the order in which neurons are called or how layers of neurons interact), as well as the laws by which they operate (composition).
Imagine that a neuron in a neural network is a pure function that calls other functions in a certain order. The same can be pictured when particular layers interact within a neural network. A layer is a collection of neurons.
The order of construction (the architecture) — the arrangement of layers relative to each other and which neurons belong to which layers — is determined by the creator of the neural network.
So in essence: — layers are collections of functions (modules of functions); — neurons are functions; — morphisms are the order in which some functions call others; — the weights in a neural network are essentially the arguments we pass when calling a neuron, that is, a function.
// ==== Визуализация данных ====
-------------------
| Ваш вопрос |
-------------------
||
▼
-------------------
| Слой математики |
-------------------
||
▼
-------------------
| Слой анализа |
-------------------
||
▼
-------------------
| Слой вывода |
-------------------
-------------------
// можно представить и как композицию функций
const mathModule = pipe (
Math.sin, // совокупность функций
Math.cos,
...
)
// logicModule :: Number :: String
const logicModule = (x) => x > 0 ? 'Положительное' : 'Отрицательное'
// langModule :: Number -> String
const langModule = (x) => `Ответ: ${x}`
// answer :: String -> String
const getAnswer = pipe (
logicModule, // модуль логики
mathModule, // модуль математики
langModule, // модуль языковой
)
// вопрос пользователя
const question = prompt('Отвечу на вопросы по математике')
// получаем ответ для пользователя
const answer = getAnswer(question)
console.log (answer) // ответ на вопрос16 The map, filter, and reduce functions
The most frequently used functions in functional programming and production code are map, filter, and reduce.
These functions provide powerful capabilities for working with data.
map --------------------
The map function returns a new object of the same data type, passing each element through the function it receives as an argument when the type is iterable. If the type is not iterable, the contained data is simply passed through the function.
Let's implement the map function in several different ways.
// каррирование используется, чтобы
// нам было удобно вызывать и в композиции
// также же используется классический
// бесточечный стиль
// ==== реализация декларативная ====
// mapD :: (a -> b) -> [a] -> [b]
const mapD = (fn) => (arr) => arr.map(fn)
// ===================================
// ==== реализация императивная ====
// без индекса итерации и контекста
// mapI :: (a -> b) -> [a] -> [b]
const mapI = (fn) => (arr) => {
// массив результирующий
// который будем возвращать
let result = []
for (const item of arr) {
// каждый элемент пропускаем через
// функцию первого класса
result.push(fn(item)) // или result = [ ...result, fn (item) ]
}
return result
}
// ===================================
// ==== реализация императивная ====
// с индексом итерации и контекстом
// mapIq :: (a -> b) -> [a] -> [b]
const mapIq = (fn) => (arr) => {
let result = []
for (let i = 0; i < arr.length; i++ ) {
result.push(fn(arr[i], i, arr)) // или result = [ ...result, fn (arr[i], i, arr) ]
}
return result
}
// ===================================
// ==== реализация рекурсивная ====
// с трамплина чтобы стэк не переполнился
// надеюсь вы прочитали раздел про рекурсию
// mapR :: (a -> b) -> [a] -> [b]
const mapR = (fn) => (arr, acc = []) => {
// базовый случай
// когда массив пуст возвращаем аккумулятор
if (!arr.length) {
return acc
}
// рекурсивный случай
// возвращаем функцию для трапмплина
// вырезаем первый элемент массива
// в аккумулятор кладем результат который пропустили через функцию
return () => mapR (fn)
(
arr.slice(1),
acc = [...acc, fn (arr[0])]
)
}
const mapTrampoline = trampoline(mapR)
// ===================================
// ==== запуск ====
const data = [1, 2, 3, 4, 5]
const result = mapD (v => v * 2) (data)
console.log (result) // [2, 4, 6, 8, 10]
// ===================================filter --------------------
The filter function returns a new object of the same data type, passing each element through a predicate function that returns the element into the object if the predicate returns true (the type is iterable).
A predicate is a function that returns true or false. It is typically used as a first-class function, meaning it is passed into another higher-order function such as filter.
Let's implement the filter function in several different ways.
// каррирование используется, чтобы
// нам было удобно вызывать и в композиции
// также же используется классический
// бесточечный стиль
// ==== реализация декларативная ====
// filterD :: (a -> Boolean) -> [a] -> [a]
const filterD = (fn) => (arr) => arr.filter(fn)
// ===================================
// ==== реализация императивная ====
// без индекса итерации и контекста
// filterI :: (a -> Boolean) -> [a] -> [a]
const filterI = (fn) => (arr) => {
// массив результирующий
// который будем возвращать
let result = []
for (const item of arr) {
// каждый элемент пропускаем через
// функцию предикат
// если она вернет true кладем ее
// в результирующий массив
if (fn(item)) {
result.push(item) // или result = [ ...result, item ]
}
}
return result
}
// ===================================
// ==== реализация императивная ====
// с индексом итерации и контекстом
// filterIq :: (a -> Boolean) -> [a] -> [a]
const filterIq = (fn) => (arr) => {
let result = []
for (let i = 0; i < arr.length; i++ ) {
const value = arr[i]
if (fn(value, i, arr)) {
result.push(value) // или result = [ ...result, value ]
}
}
return result
}
// ===================================
// ==== реализация рекурсивная ====
// с трамплина чтобы стэк не переполнился
// надеюсь вы прочитали раздел про рекурсию
// filterR :: (a -> Boolean) -> [a] -> [a]
const filterR = (fn) => (arr, acc = []) => {
// базовый случай
// когда массив пуст возвращаем аккумулятор
if (!arr.length) {
return acc
}
// рекурсивный случай
// возвращаем функцию для трапмплина
return () => {
// первый элемент и остальные
const [head, ...tail] = arr
// в аккумулятор кладем элемент
// если функцию предикат вернула true
// иначе возвращаем текущий аккумулятор
// без добавления значения
const nextAcc = fn (head)
? [...acc, head]
: [...acc]
return filterR (fn)(tail, nextAcc)
}
}
const filterTrampoline = trampoline(filterR)
// ===================================
// ==== запуск ====
const data = [1, 2, 3, 4, 5]
const result = filterD (v => v > 3) (data)
console.log (result) // [4, 5]
// ===================================reduce --------------------
The reduce function can return any data type; it takes a function and an accumulator as its arguments.
As arguments, the function takes the accumulator (from the initial value or from subsequent calls) and the current element, passes this data through the function's logic, and returns a new accumulator or applies changes to the current accumulator.
The accumulator (the initial value) can be any value.
With reduce you can emulate any custom behavior, as well as the behavior of map and filter. But it is usually used to collect specific data according to a particular logic from an iterable object type.
// каррирование используется, чтобы
// нам было удобно вызывать и в композиции
// также же используется классический
// бесточечный стиль
// ==== реализация декларативная ====
// reduceD :: ((a, b) → a) → a → [b] → a
const reduceD = (fn) => (init) => (arr) => arr.reduce(fn, init)
// ===================================
// ==== реализация императивная ====
// без индекса итерации и контекста
// reduceI :: ((a, b) → a) → a → [b] → a
const reduceI = (fn) => (init) => (arr) => {
// значение которое будем возвращать
// устанавливаем ему инициализационное значение
let accumulator = init
for (const item of arr) {
// вызываем функцию
// с первым аргументом аккумулятором
// вторым аргументом текущим элементом массива
// результат вызова кладем в аккумулятор
// чтобы при след вызове результат предыдущего сохранялся
accumulator = fn (accumulator, item)
}
return accumulator
}
// ===================================
// ==== реализация императивная ====
// с индексом итерации и контекстом
// reduceIq :: ((a, b, Number, [b]) → a) → a → [b] → a
const reduceIq = (fn) => (init) => (arr) => {
let accumulator = init
for (let i = 0; i < arr.length; i++ ) {
accumulator = fn (accumulator, arr[i], i, arr)
}
return accumulator
}
// ===================================
// ==== реализация рекурсивная ====
// с трамплина чтобы стэк не переполнился
// надеюсь вы прочитали раздел про рекурсию
// reduceR :: ((a, b) → a) → a → [b] → a
const reduceR = (fn) => (init) => (arr) => {
// базовый случай
// когда массив пуст возвращаем аккумулятор
if (!arr.length) {
return init
}
// рекурсивный случай
// возвращаем функцию для трапмплина
return () => {
// первый элемент и остальные
const [head, ...tail] = arr
// аккумулятор который будем возвращать
// пропускаем через функцию
// старый аккумулятор и текущее значение
const nextInit = fn (init, head)
// вызываем заново функцию
// со старой функций
// новым аккумулятором пропущенным через функцию
// и срезаем массив с данными
return reduceR (fn) (nextInit) (tail)
}
}
const reduceTrampoline = trampoline(reduceR)
// ===================================
// ==== запуск ====
const data = [1, 2, 3]
const result = reduceD ((a, b) => a + b) (0) (data)
console.log (result) // 6
// ===================================17 First-class and higher-order functions
Functions in JavaScript can be both first-class functions and higher-order functions.
First-class functions is the term commonly used for functions that are passed as an argument to another function.
Higher-order functions is the term commonly used for functions that accept other functions as arguments.
// Простой пример
// ===============================
const data = [1, 2, 3, 4]
// эта функция первого класса
// потому что она выступает в качестве
// аргумента для другой функции
// multiply :: Number -> Number
const multiply = (v) => v * 2
// функция map это функция
// высшего порядка
// потому что принимает другую функцию
// в качестве аргумента
const result = data.map(multiply)
console.log (result) // [2, 4, 6, 8]In short, the receiving function is the higher-order function, and the function that is passed in when the function is called is the first-class function.
In Vue JS and React JS, components themselves can act as both higher-order functions and first-class functions, since components are in fact functions.
// Простой пример
// ===============================
// hof :: (* -> *) -> * -> *
const hof = (fc) => (...args) => fc (...args)
// функция hof - это функция высшего порядка
// так как в качестве первого аргумента принимает
// функцию первого класса
// функция fc - функция первого класса
// так как она способна выступать в качестве
// аргумента для функции
// Пример из Vue JS
// HOF (HOC)
// Higher Order Function (Higher Order Component)
// Функция высшего порядка (Компонент высшего порядка)
// принимает в слот другой компонент
// под капотом
function MyComponent({ main, default: def } = {}) {
const greetingMessage = 'hello'
if (main) {
return `<div>main - ${ main (greetingMessage)}</div>`
}
if (def) {
return `default - ${def(greetingMessage)}`
}
return 'нет компонентов'
}
// вызов hof
const templateMain = MyComponent({
main: (text) => `XXX${text}XXX`
})
const templateDefault = MyComponent({
default: (text) => `${text}`
})
console.log(templateMain) // main - XXXhelloXXX
console.log(templateDefault) // default - hello18 Lenses
Lenses are a tool (a pattern) used in functional programming when working with data, providing data immutability and function purity.
Immutability is the ability not to mutate data.
Let's take an example from the official documentation of the Ramda JS library.
// Lens s a = Functor f => (a → f a) → s → f s
// ===============================
// создаем линзу
// путь к данными
const xHeadYLens = R.lensPath(['x', 0, 'y']);
// запускаем линзу в разных функциях
// тестируемые данные
const data = { x: [ { y: 2, z: 3 }, { y: 4, z: 5 } ] }
// функция view получает данные согласно пути линзы
// не меняя ничего в первоначальном объекте
const resultView = R.view(xHeadYLens, data);
// функция set устанавливает новые данные
// по пути линзы не меняя исходные данные в объекте
// а возвращая новый объект с измененными данными
const resultSet = R.set(xHeadYLens, 1, data);
// over работает с линзой как set
// но вместо данных принимает функцию
// которая примет в качестве аргумента
// данные по пути линзы
const resultOver = R.over(xHeadYLens, R.negate, data);
console.log(resultView) // 2
console.log(resultSet) // {x: [{y: 1, z: 3}, {y: 4, z: 5}]}
console.log(resultOver) // {x: [{y: -2, z: 3}, {y: 4, z: 5}]}In the examples above, we saw how to use lenses.
And now let's try to implement our own custom lens. In production code, use ready-made solutions from libraries like Ramda JS or Sanctuary JS.
// Lens s a = Functor f => (a → f a) → s → f s
// ===============================
// создаем линзу
// lens :: [String] -> Lens s a
const lens = (path) => {
// getter :: [a] -> [b] -> b
const getter = (path) => (data) => {
// если в линзу не прокинут путь
// то возвращаем данные
if (!path.length) {
return data
}
// первый элемент пути и остальной путь
const [head, ...tail] = path
// когда в пути остается один элемент
// хвост отсутствует
// вызывается базовый случай
// и возвращаются данные
if (!tail.length) {
return data[head]
}
// рекурсивный случай
// срезанный путь бросаем в качестве первого аргумента
// в качестве данные бросаем вложенность
return getter (tail) (data[head])
}
// setter :: [a] -> b | (* -> *) -> [b] -> [c]
const setter = (path) => (v) => (data) => {
// если в линзу не прокинут путь
// то возвращаем данные
if (!path.length) {
return data
}
// первый элемент пути и остальной путь
const [head, ...tail] = path
// когда в пути остается один элемент
// хвост отсутствует
// вызывается базовый случай
// и возвращаются данные
// подменив значение ключа
if (!tail.length) {
// проверка чтобы понять
// вызывается как over или set
// чисто функция или значение
// отправлено в качестве аргумента
const isFunction = typeof v === 'function'
return {
...data,
[head]: isFunction
? v (data[head])
: v
}
}
// рекурсивный случай
// срезанный путь бросаем в качестве первого аргумента
// аргумент изменения оставляем такой же
// в качестве данные бросаем вложенность
return {
...data,
[head]: setter (tail) (v) (data[head])
}
}
// возвращает геттер и сеттер
// сохраняя в замыкании путь к данным
return {
getter: getter (path),
setter: setter (path),
}
}
// линза готова
// осталось реализовать view set over
// мы просто вызываем геттер у линзы
// и бросаем туда данные для обработки
// view :: [a] -> [b] -> b
const view = (lens) => (data) => lens.getter (data)
// мы просто вызываем сеттер у линзы
// и бросаем туда данные для обработки
// set :: [a] -> c -> [b] -> b
const set = (lens) => (v) => (data) => lens.setter (v) (data)
// аналогичен в реализации set
// over :: [a] -> c -> [b] -> b
const over = (lens) => (fn) => (data) => lens.setter (fn) (data)
// тестируем код
// =============
// создаем линзу
const xLens = lens(['x', 0, 'y']);
// тестируемые данные
const data = { x: [ { y: 2, z: 3 }, { y: 4, z: 5 } ] }
const resultView = view (xLens) (data)
const resultSet = set (xLens) (1) (data)
// ((a) => (b) => a + b) (3) создается каррированая функция
// которая сразу вызывается и возвращает
// (b) => 3 + b
const resultOver = over (xLens) ( ((a) => (b) => a + b) (3) ) (data)
console.log(resultView) // 2
console.log(resultSet) // {x: [{y: 1, z: 3}, {y: 4, z: 5}]}
console.log(resultOver) // {x: [{y: 5, z: 3}, {y: 4, z: 5}]}19 Side effects in function composition
Based on the principles of functional programming, one might think that creating side effects within function composition is forbidden. But in reality, any program, and especially web development, will create some side effect no matter what (DOM operations, input/output operations, logging, and so on ...).
In real-world cases, side effects within function composition can be useful to us mainly in two situations:
1. Debugging the composition and logging it, in case of errors or simply to understand the execution flow and how the input data changes within the function composition.
2. Creating a side effect within the program according to its logic and continuing the execution of the function composition.
Let's try to inspect the intermediate results within a function composition by creating a side effect in the form of logging the data.
// Программа, которая берет деньги
// пользователя и сравнивает с ценой товара
// сообщает хватает ли ему денег на покупку товара
// ===============================
// pipe :: ((a -> b), (b -> c), ..., (y -> z)) -> a -> z
const pipe = (...fns) => (data) =>
fns
.reduce((init, fn) => fn(init), data)
// пользователь тестовый с 10 000$
const dataUser = {
name: 'John',
money: 10000,
}
// товар с ценной 8 000$
const product = {
name: 'car',
model: 'toyota',
price: 8000
}
// prop :: String -> Record -> a
const prop = (prop) => (data) => data[prop]
// lt :: Number -> Number -> Boolean
const lt = (a) => (b) => a < b
const pipeline = pipe(
prop ('money'), // получаем деньги пользователя
lt (prop ('price') (product)), // сравниваем цену товара и деньги пользователя
(success) => success ? 'the product can be purchased' : "you don't have enough money"
// сообщаем может ли купить товар или не хватает денег
)
// запускаем композицию
const resultMsg = pipeline (dataUser)
console.log(resultMsg) // the product can be purchasedBut how do we obtain the results of each function call in pipeline (prop ('money') ...), if there won't even be any errors in the composition and we simply want to debug the code.
Let's write a universal utility for any case in the chain of composition calls.
// Утилита которая вызовет функцию в аргументе
// и прокинет аргумент дальше по композиции
// ===============================
// tapD :: (a → *) → a → a
const tapD = (fn) => (arg) => {
// делаем проверку
// действительно ли первый аргумент функция
if (typeof fn === 'function') {
// вызываем функцию с аргументом
fn (arg)
}
// не забываем дальше прокинут аргумент
// для вызова в композиции функций
return arg
}
// можно сократить в одну строку
// tap :: (a → *) → a → a
const tap = (fn) => (arg) => (fn (arg), arg)
// а теперь давайте подготовим утилиту
// для логирования данных
// после каждого вызова в композиции
// стандартный вывод в консоль
const log = console.log
// создаем логгер
const logger = tap (log)
// 😎 вводим логгер в цепочку вызовов функций
// сразу запустим цепочку
pipe(
prop ('money'),
logger, // 10000
lt (prop ('price') (product)),
logger, // true
(success) => success ? 'the product can be purchased' : 'you don"t have enough money',
logger, // 'the product can be purchased'
) (dataUser)20 Vanilla JS, Lodash, Ramda, Sanctuary -> which one to choose
When you face the choice of which of the libraries described above, or a vanilla solution, to pick, you should not rely on the following principles, which I have often encountered in my own commercial development practice. You should not stick to the following:
1. I like this library (personal wishes). 2. It's trendy right now ))) (personal wishes). 3. I'm in charge, I decide, and that's what I want (personal wishes). Everything described above is not a professional approach.
What you should pay attention to. Which criteria matter when choosing:
1. Budget. It is very important to understand the financial capacity of the investor (client) and their plans for the future. This is a very important point and, you could say, one of the main ones. 2. How far along the project is, percentage-wise, or whether it is just at the beginning. Likewise: if the project is already written and everything works correctly, there is no need to rewrite it. 3. Who works with you on the team and how many of you there are. You need to understand the onboarding process for a new developer joining the team, whether they are familiar with this library and with your architectural decisions. 4. Product quality. The higher the complexity, the more the quality tends to grow. 5. Performance. This is not the most important factor, since often it is a flaw in the application's logic that causes a slowdown rather than any particular library. 6. The functional programming paradigm.
| Vanilla JS | ----------------
1. Budget. The budget is small or medium. 2. How far along the project is, percentage-wise, or whether it is just at the beginning. When the project was already started earlier in JS and has passed the horizon and the home stretch. 3. Who works with you on the team and how many of you there are. For small teams with deep knowledge of JS. At times this will be more effective than large teams with shallow knowledge of JS who supposedly write without errors in TS. 4. Product quality. Depends on the team, deadlines, and budget; often not high. 5. Performance. It will be the highest given equivalent code compared to the libraries. 6. The functional programming paradigm. Complete freedom: procedural, OOP, FP, and so on. And everyone does as they please.
| Lodash | ----------------
1. Budget. Suitable for projects of any scale, but it is best used on medium-sized projects. 2. How far along the project is, percentage-wise, or whether it is just at the beginning. Only if the project has not reached the horizon but is at an early stage. 3. Who works with you on the team and how many of you there are. For teams of any size who are superficially familiar with functional programming and want to simplify their routine. 4. Product quality. Depends on the team, deadlines, and budget; often not high. 5. Performance. It will be lower than vanilla given equivalent code. The library's bundle is optimized for size. 6. The functional programming paradigm. Not entirely functional: there are problems with immutability and with passing arguments in function composition, where the functions take the data first and only then the function. For those who are only just touching functional programming.
| Ramda | ----------------
1. Budget. Suitable for projects of any scale. 2. How far along the project is, percentage-wise, or whether it is just at the beginning. At any stage, except for situations when the project is already wrapping up or another data-handling library is already present. 3. Who works with you on the team and how many of you there are. For teams of any size who use functional programming either superficially or professionally and who want to simplify their routine. 4. Product quality. The product quality is, as a rule, high. 5. Performance. It will be lower than vanilla given equivalent code. The library's bundle needs to be optimized if you plan to use only a small part, 5-10%, of its functions. 6. The functional programming paradigm. Full-fledged functional programming is used. It covers up to 98% of cases when working with data and is suitable for all projects. It has its own philosophy within functional programming. It is easier for newcomers to functional programming and sets the right tone. It does not have Lodash's ailments.
| Sanctuary | ----------------
1. Budget. Suitable for large-scale projects with an above-average budget. 2. How far along the project is, percentage-wise, or whether it is just at the beginning. It is preferable to use it at the early stage. 3. Who works with you on the team and how many of you there are. For teams of any size who use functional programming professionally and understand what they are writing. 4. Product quality. The product quality is, as a rule, high. 5. Performance. It will be lower than vanilla given equivalent code. 6. The functional programming paradigm. Full-fledged functional programming is used, with its own philosophy that adheres more strictly to FP principles. Full-fledged use of monads and other abstract types. A high barrier to entry. For those who are truly into functional programming. Its own architecture of thinking.
One can picture the degree of commitment to functional programming.
Vanilla JS (present) -> Lodash (not full-fledged) -> Ramda (full-fledged) -> Sanctuary (full-fledged and strict)
Comparison table. Budget: Vanilla JS — Small / Medium, Lodash — Any, Ramda — Any, Sanctuary — Medium / High. Project stage: Vanilla JS — Any, Lodash — Early, Ramda — Any, Sanctuary — Early. Team size: Vanilla JS — Small, Lodash — Any, Ramda — Any, Sanctuary — Any (pros). Quality: Vanilla JS — Medium, Lodash — Medium, Ramda — High, Sanctuary — High. Performance: Vanilla JS — 4 out of 5, Lodash — 3 out of 5, Ramda — 2 out of 5, Sanctuary — 2 out of 5. FP strictness: Vanilla JS — 1 out of 5, Lodash — 2 out of 5, Ramda — 3 out of 5, Sanctuary — 4 out of 5. Barrier to entry: Vanilla JS — Low, Lodash — Low, Ramda — Medium, Sanctuary — High. Philosophy: Vanilla JS — Freedom of choice, Lodash — Utility, Ramda — Practical FP, Sanctuary — Academic FP.