Курс по функциональному программированию на JS



  1. I
    -> Для кого этот курс

  2. II
    -> Кому не подойдёт

  3. III
    -> Императивность и декларативность

  4. IV
    -> Функциональное программирование

  5. V
    -> Чистые функции

  6. VI
    -> Нотация типов Хиндли–Милнера

  7. VII
    -> Теория категорий

  8. VIII
    -> Fantasy-land (Страна фантазий)

  9. IX
    -> Композиция функций

  10. X
    -> Каррирование

  11. XI
    -> Каррирование и Асинхронность (Эволюция)

  12. XII
    -> Частичное применение

  13. XIII
    -> Рекурсия

  14. XIV
    -> Производительность (Performance - Перфоманс)

  15. XV
    -> Нейронные сети и теория категорий

  16. XVI
    -> Функции map, filter, reduce

## Для кого этот курс

Этот курс в первую очередь написан для разработчиков с опытом разработки в JS, которые хотят расширить свой кругозор и писать более надежный, декларативный и сопровождаемый код в функциональном стиле.

## Кому этот курс не подойдет

Этот курс не подойдет начинающим разработчикам, которые только постигают азы разработки на JS.

## Императивность и декларативность

Императивный код говорит вам "как вы это делаете". То есть Вам надо вникать в сам код.

Декларативный код Вам говорит, что делает ваш код, он чаще модульный, и вам не нужно. вникать в весь код. Если требуются изменения, вы вносите изменений в определенный участок кода модульно.

    
      
            // например получить все числа больше 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
            // ======

          
    
  

## Функциональное программирование

Функциональное программирование - это высокоуровневая декларативная разработка на основе чистых функций. Функциональное программирование - парадигма программирования, которая опирается на функции как на основные строительные блоки.

Чистые функции - это функции, которые при и одних и тех же аргументах возвращают одинаковый результат и не создают побочных (сайд) эффектов.

    
      
          // ==== Внимание ===
          // Эти примеры Вам пока могут казаться крайне запутанными и непонятными
          // Не пугайтесь и читайте дальше

          // наша цель
          // 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'
        
    
  

## Чистые функции

Чистые функции крайне важны и обычно они идемпотентны, но не всегда. Если вам нужны сайд эффекты то группируйте их в отдельные функции и разделяйте от чистых функций. Любая программа особенно в вебе нуждается в сайд эффектах (изменение Dom дерева (tree), запросы к серверу (fetch), чтение из файла (readFile) и тп)

    
      

          // НЕчистая функция
          // ===============================
          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 } получили нового пользователя без сайд эффектов
          // ===============================
        
    
  

## Нотация типов Хиндли–Милнера

Нотация типов Хиндли–Милнера вы уже встречали в коде. Над функциями в блоке комментариев.

Эта нотация типов используется в Haskell и других языках с функциональным уклоном. В JS она помогает нам писать более абстрактный код, а также помогает сопровождать этот код.

Дает объяснение какие типы аргументов принимает функция и что возвращает.

    
      
          // без нотации
          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)

        
    
  

## Теория категорий

Существует тесная связь между функциональным программированием и теорией категорией. В функциональном программировании многие абстракции, законы, принципы, взяты из теории категорий.

Что же мы знаем о теории категорий. Мы не будем углубляться в основы и академические термины. Мы постараемся объяснить все это без академических терминов.

Теория категорий в рамках программирования - это совокупность абстрактных объектов (любые типы данных Array, Object, Number, String ...) и их морфизмов (стрелок их отношения то есть функции), а также законов по которым они работают (композиция и тп).

    
      
        //  Пример
        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 -> c


        
    
  

А давайте мы с вами пофантазируем, чтобы глубже понять теорию категорий. Используем аналогии из окружающего нас мира.

Мы используем природу и напишем псевдокод реализующий теорию категорий.

    
      
        //  Пример
        a -> b -> c
        твердое -> жидкое -> газообразное
        лёд -> вода -> пар

        лёд, вода, пар // это объекты
        -> // процессы над ними (нагрев и тп)
        a ... c // процесс композиции (упорядоченная совокупность процессов)

        // Практический пример

        // превратитьВВоду :: лёд -> вода
        const превратитьВВоду = лёд => (держим в комнатной температуре, вода)

        // превратитьВПар:: вода -> пар
        const превратитьВПар = вода=> (производим сильный нагрев, пар)

        // композицияПроцессов :: (* -> *) -> (* -> *) -> * -> *
        const композицияПроцессов = (f2, f1) => (...args) => f2(f1(...args))

        const pipeline = композицияПроцессов(
          превратитьВПар,
          превратитьВВоду
          )

        // start
        pipeline(лёд) // пар результат

        
    
  

## Fantasy-land (Страна фантазий)

Спецификация Fantasy-land (Страна фантазий) - это набор правил, публичных интерфейсов описывающих абстракции из теории категорий. *подробнее

Эти правила и законы опираются на алгебраические структуры данных.

По сути реализовывая эти интерфейсы и законы, мы следуем спецификации Fantasy-land (Страна фантазий)

    
      

          // Вот как она выглядит

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)

Давайте мы попробуем с Вами создать Functor согласно Fantasy-land (Страна фантазий)

    
      
          // Сигнатура
          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

          // =========================
        
    
  

## Композиция функций

Композиция функций - это способность функций вызываться последовательно, где результат одной функции передаётся следующей.

Композиция функций это одно из фундаментальных понятий, на которых основана теория категорий.

    
      

          // === композиция из 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)
                  )
              )

          // ===============================
        
    
  

Если вы заметили в примере выше, порядок выполнения функций был справа налево, с точки зрения математики это ок. Но часто читателю удобнее читать код слева направо.

Поэтому код выше мы можем переписать.

    
      

          // === композиция из 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

          // ===============================
        
    
  

А теперь представим ситуацию когда количество функций более 3.

В реальных проектах используйте compose или pipe с готовых библиотек. Мы попробуем создать compose несколькими способами (Императивно) (Декларативно) (Рекурсивно)

    
      

          // === композиция функций в нестрого заданном количестве функций ===

          // === Способ императивный ===
          // порядок сменился на слева на право
          // 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
          }

          // ===============================
        
    
  

А теперь давайте реализуем в функциональном стиле декларативно.

    
      
        // === композиция функций в нестрого заданном количестве функций ===

        // === Способ декларативный ===
        // порядок сменился на слева на право
        // 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 // аргументы первого вызова
        )

        // ===============================
        
    
  

Мы также можем реализовать композицию функций рекурсивно. Сразу сделаем оптимизацию хвостовой рекурсии и сократим количество вызовов вдвое.

    
      
        // === композиция функций в нестрого заданном количестве функций ===

        // === Способ рекурсивный ===
        // порядок сменился на слева на право
        // 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))
        }

        // ===============================
        
    
  

## Каррирование

Каррирование - это один из приемов используемых в функциональном программировании, при котором происходит вычислении функции только при полном получении всех аргументов, а при частичном получении аргументов функция возвращает новую функцию.

Каррирование может быть использовано

1. В композиции функции, когда функция в последовательности вызовов, ожидает два и более аргумента.

2. Когда мы можем создавать специализированные функции на основе базовой функции меняя значение аргумента в замыкании

    
      

        //  Пример
        // Каррирование
        // 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) // 15

        
    
  

Как же нам каррировать любую функцию с n количеством параметров, значение которых мы не знаем. Например 5 параметров и более у функции.

Можно использовать автоматическое каррирование любой функции и вызывать ее в любом порядке

    
      

        // Например мы хотим каррировать функцию 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) ) // 3
        
    
  

А теперь давайте глянем всю мощь каррирования в примерах композиции функции.

    
      

        // Например в следующей композиции будут
        // =====================================
        // использоваться две функции каррированые
        // одна каррированая уже
        // а другая автоматически каррируем перед вызовом

        // данные с которыми будем работать
        // список пользователей
        // 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 ), // вернуть имена
        )

        
    
  

Мы также можем создавать специализированные функции при помощи каррирования. Это нечто подобное "наследованию" в ООП.

Берется базовая каррированная функция, а дальше при помощи вызовов создаются другие функции. Ну хватит болтать)) давайте к примерам сразу приступим.

    
      

        // Давайте возьмем каррированную функцию
        // и начнем создавать другие специализированные функции

        // Базовая функция сравнения значения по определенному ключу
        // 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) ) {
          // что то там делам ...
        }

        
    
  

## Каррирование и Асинхронность (Эволюция)

Каррирование можно эффективно использовать в асинхронном программировании.

1. Создание специализированных функций

2. Победим "Ад колбеков" без промисов

    
      

        //  Давайте создадим базовую функцию для запросов
        // 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')

        
    
  

А теперь внизу мы покажем два примера.

Первый код будет императивный на колбеках, а второй будет использовать каррирование и замыкание без промисов.

    
      

        //  код который "плохо пахнет"

        // создадим функцию для запросов на основе 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
              })
            })
          })


        
    
  

Мы используем каррирование без промисов и победим "ад колбеком"

Отрефакторим наш код.

    
      

        //  декларативный код
        // ==== ВНИМАНИЕ многие функции хелперы уже
        // ==== существуют в готовых библиотеках
        // ==== тут мы это часто показываем в учебных целях

        // создадим функцию для запросов на основе 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) // результат записываем в глобальную переменную
                    })
                )
              )
        ))

        
    
  

Я Вам признаюсь ))) запах плохо кода не пропал.

А что если мы добавим мощь Промисов в код выше и преобразим его.

    
      

        //  А теперь возьмемся за дело
        // перепишем нашу функцию получения данных
        // которая сама возвращает промис

        // 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, // устанавливаем значением в глоб переменную
        ))

        
    
  

## Частичное применение

Частичное применение - это один из приемов используемых в функциональном программировании, при котором происходит вычислении функции только при полном получении всех аргументов.

Частичное применение очень похоже на каррирование, но есть отличия.

1. При частичном применении количество отложенных вызовов обычно не больше двух.

2. Количество аргументов передаваемых в функцию при частичном применении может быть несколько, а в классическом каррировании обычно одно.

    
      

          //  пример функции с частичным применением
          // 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) ) // 3

        
    
  

## Рекурсия

Рекурсия - это способность функции вызывать саму себя.

Рекурсия должна отвечать двум основным требованиям, чтобы она корректно работала.

1. Базовый случай. Это логика при которой рекурсия должна прекращаться. Базовое условие, которое прекращает вызов функции саму себя.

2. Рекурсивный случай. Это логика при которой функция вызывает саму себя, обычно с небольшими изменениями в аргументах.

Рекурсия часто используется в функциональном программировании.

    
      

          // наша цель получить средний возраст пользователей

          // ================================
          // декларативный стиль без рекурсии

          // список пользователей
          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
          }
          )()

        
    
  

Пример выше с рекурсия возможно для некоторых не простой. Поэтому мы напишем пример проще и визуализируем вызова. Главное не волнуйтесь, если сразу вам что-то непонятно.

    
      

          // Наша цель собрать общий рейтинг пользователей
          // То есть сумму  рейтингов

          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.5

        
    
  

В примере выше в функции "rateSum" есть один существенный недостаток. Это количество вызовов, которое можно оптимизировать, используя прием оптимизация хвостовой рекурсии.

Зачем нам это нужно. В JS стек вызовов функции при рекурсивном синхронном вызове переполняется, так как функции создаются не лениво, то есть они создаются уже до момента вызова в отличие от haskell, где нет циклов и функции, создаются лениво в момент фактического вызова.

    
      

          // === Оптимизация хвостовой рекурсии ====
          // Давайте избавимся от лишних вызовов
          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
          // дальше вызовы прекращаются
          // удержания и возвратов нет
          // размотки стека нет
          // возвращаем итоги
          // количество вызовов сократилось вдвое

        
    
  

Оптимизация хвостовой рекурсии на текущий момент времени 2025 год к сожалению не реализована в движке Node JS (Chrome). Она была реализована в одной из версии Core JS (Safari), но дальше была удалена. В любом случае количество вызовов сокращается. В продакшен коде старайтесь не использовать её когда не владеете точной информацией о количестве вызовов в программе.

Далее в разделе рекурсии мы будем использовать приемы, при помощи которых можно безопасно использовать рекурсию в продакшен коде без подсчета количества вызовов.

Так как же нам освободить стек вызовов без её переполненияи в продакшен коде.

1. Сделать код асинхронным и не блокировать поток и освобождать стек

2. Использовать санку (thunk) "Трамплин" - фейковую рекурсию

    
      

          // === 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

          // Минусы этого подхода
          // Скорость выполнения (производительность) низкая
          // Если вам необходимы длительные операции вызовы функций синхронные
          // То лучше использовать веб воркеры в отдельном потоке
          // При условии, что у вас процессор больше одного ядра

        
    
  

Разберем логику использования трамплина, чтобы избежать переполнения стека. Трамплин работает синхронно в отличие от вызова через таймер.

Рассмотрим основные принципы трамплина.

1. Трамплин - это функция обертка, которая придает нашей основной функции особенное поведение.

2. Трамплин функция возвращает другую функцию, которая принимает аргументы.

3. Внутри она вызывает в цикле функцию основную с аргументом.

4. Если возвращаемое значение функция, то она ее вызовет опять иначе остановит цикл и вернет результат.

5. Основная функция в базовом случае должна возвращать результат.

6. Основная функция в рекурсивном случае должна обязательно возвращать функцию.

    
      

          // === Рекурсия через трамплин ====

          // обратите внимание первый аргумент это функция
          // а вторым аргументом мы по сути запускаем вызов функции
          // 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
          ]

        
    
  

## Производительность (Performance - Перфоманс)

Следует отметить, что в типичных случаях императивный код работает немного быстрее, однако разница обычно незначительна. Иногда функциональный код может показывать даже те же или лучшие результаты благодаря оптимизациям движка JavaScript. Если на одной чаше весов удобство сопровождения кода, надёжность кода, правильная архитектура , то безусловно лучше выбирать функциональный код.

За улучшение скорости работы функционального кода в первую очередь отвечают разработчики движков JavaScript. То есть разработчики движков проводят улучшения, оптимизации компилятора, сборщика мусора.

Каждая абстракция создает дополнительный небольшой расход ресурсов, зачастую он не критичен.

В проектах, где критически важна скорость и количество элементов достигает несколько десятков тысяч и даже сотен тысяч при синхронном выполнении кода, и мы не можем создать отдельный поток в силу отсутствия мощностей железа, а именно свободного ядра процессора, мы можем прибегнуть к низкоуровневым императивным инструментам.

В 98% задач скорость работы вашего приложения скорее определяется правильной логикой построения программы, а не лишней абстракцией.

    
      

          // Произведем небольшие замеры
          // ===============================
          // функция, которая генерирует массив чисел от 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 уже по сути нативное решение ))
          // Это не противоречие моим словам выше это просто нативное улучшение
          // Наши абстракции будут чуть больше использовать

        
    
  

Из выше сказанного можно сделать вывод, что нативные решения более эффективны.

А давайте напишем небольшую утилиту для замеров. И проведем тесты скорости выполнения.

    
      

          // Утилита замера скорости
          // ===============================
          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

          // вот тут мы и выяснили, что все таки императивный код быстрее )))

        
    
  

## Нейронные сети и теория категорий

Давайте с Вами попробуем рассмотреть работу нейронных сетей в рамках теории категорий. Для этого мы вначале повторим понятие теории категорий о которой мы говорили в предыдущих главах.

Теория категорий - это совокупность абстрактных объектов (любых) и их морфизмов (стрелок их отношений), а также законов по которым они работают.

Теория категорий в рамках нейронных сетей - это совокупность абстрактных объектов (нейронов или слоев с нейронами) и их морфизмов (стрелок то есть порядок вызова нейронов или взаимодействия слоев с нейронами), а также законов по которым они работают (композиция).

Представим себе, что нейрон в нейросети это чистая функция и она вызывает другие функции в определенном порядке. Тоже самое можно представить когда определенные слои взаимодействуют в нейронной сети. Слой - это совокупность нейронов.

Порядок построения (архитектура) местонахождение слоев между собой и каких нейронов в каких слоях определяется создателем нейросети.

То есть по сути
- слои это совокупность функций (модули с функциями)
- нейроны это функции
- морфизмы это порядок вызова функций одних другими.
- Веса в нейронной сети это по сути аргументы, которые мы прокидываем при вызове нейрона, то есть функции.

    
      

          // ==== Визуализация данных ====

          -------------------
          |   Ваш вопрос     |
          -------------------
                  ||
                  ▼
          -------------------
          |  Слой математики |
          -------------------
                  ||
                  ▼
          -------------------
          |  Слой анализа    |
          -------------------
                  ||
                  ▼
          -------------------
          |  Слой вывода     |
          -------------------

          -------------------

          // можно представить и как композицию функций

          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) // ответ на вопрос
        
    
  

## Функции map, filter, reduce

Наиболее часто используемыми функциями в функциональном программировании и продакшен коде являются map, filter, reduce.

Эти функции обеспечивают мощный функционал работы с данными.

map
--------------------

Функция map возвращает новый объект того же типа данных, пропуская каждый элемент, если тип итерируемый через функцию, которая получает в качестве аргумента. Если тип не итерируемый, то просто содержимое с данными пропускается через функцию.

Давайте реализуем функцию map различными способами.

    
      

          // каррирование используется, чтобы
          // нам было удобно вызывать и в композиции
          // также же используется классический
          // бесточечный стиль

          // ==== реализация декларативная ====
          // 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
--------------------

Функция filter возвращает новый объект того же типа данных, пропуская каждый элемент через функцию предикат, которая вернет элемент в объект, если предикат вернет true (тип итерируемый).

Функция предикат - это функция, которая возвращает true или false. Она обычно используется как функция первого класса, то есть кладется в другую функцию высшего порядка как filter.

Давайте реализуем функцию filter различными способами.

    
      

          // каррирование используется, чтобы
          // нам было удобно вызывать и в композиции
          // также же используется классический
          // бесточечный стиль

          // ==== реализация декларативная ====
          // 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
--------------------

Функция reduce возвращает любой тип данных, она в качестве аргументов принимает функцию и аккумулятор.

Функция в качестве аргумента принимает аккумулятор (инициализации или последующих вызовов) и текущий элемент, пропускает через логику функции эти данные и возвращает новый аккумулятор или вносят в текущий аккумулятор изменения.

Аккумулятором (значением при инициализации) может быть любое значение.

Функцией reduce можно сымитировать любое кастомное поведение, а также поведение map and filter. Но обычно оно используется для сбора, конкретно каких то данных по определенной логике из итерируемого типа объекта.

    
      

          // каррирование используется, чтобы
          // нам было удобно вызывать и в композиции
          // также же используется классический
          // бесточечный стиль

          // ==== реализация декларативная ====
          // 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
          // ===================================

        
    
  
жмякни