#5 Композиция в Javascript и Ramda

poster
В этом видео мы с вами разберем такую тему, как композиция в javascript. Этот вопрос достаточно часто задают на собеседовании.
Понравилось? Поделитесь с друзьями!
Понравилось?
Поделитесь с друзьями!
Комментарии
Текст видео

В этом видео мы с вами разберем такую тему, как композиция в javascript. Этот вопрос достаточно часто задают на собеседовании.

Что такое композиция? Это процесс комбинирования двух или больше функций, для создания новой функции.

То есть код будет выглядеть вот так

f(g(x))

То есть сначала у нас есть какая-то переменная x, потом мы вызываем функцию g на x, а потом на результат g вызываем f.

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

То есть код

const slug = toSlug('This is composition')

должен вернуть нам строку

this-is-composition

На чистом js мы можем написать как-то так

const toSlug = input => {
  const words = input.split(' ')
  const lowercasedWords = words.map(word => word.toLowerCase())
  const slug = lowercasedWords.join('-')
  const encodedSlug = encodeURIComponent(slug)

  return encodedSlug
}

console.log(toSlug('This is composition'))

Это достаточно неплохой и читабельный код. Но в нем есть одна проблема. В процессе создания функции toSlug, мы должны были, для читабельности, создать 4 дополнительные переменные. Хотя, по факту, нас интересует только input и output, и переменные мы потом не переиспользуем. Да и код внутри переменных не особо сложный.

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

const toSlug = input => encodeURIComponent(
  input.split(' ')
    .map(str => str.toLowerCase())
    .join('-')
)

Вышло неплохо, но слабо читабельно, так как внутри encodeURIComponent идет большая конструкция.

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

const toSlug = input =>
  R.split(' ')(input)

Это разобьет нам input на слова. Мы можем так писать, потому что все методы Ramda каррируются.

Дальше нам нужно каждое слово в массиве перевести в нижний регистр.

const toSlug = input =>
  R.map(R.toLower)(
    R.split(' ')(input)
  )

То есть результат split - это массив. И мы на этот массив применяем R.map, передавая ему R.toLower. R.toLower - это аналог toLowerCase из javascript, только каррированый. Как вы видите, мы с вами вкладываем функцию в функцию, каждый раз передавая результат дальше.

Теперь добавим join.

const toSlug = input =>
  R.join('-')(
    R.map(R.toLower)(
      R.split(' ')(input)
    )
  )

И теперь вызовем encodeURIComponent на результат

const toSlug = input =>
  encodeURIComponent(
    R.join('-')(
      R.map(R.toLower)(
        R.split(' ')(input)
      )
    )
  )

Если мы посмотрим в браузер, то наш код работает так же, как и раньше.

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

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

Единственное, что сейчас хотелось бы сделать, это избавится от вложенности функций.

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

Давайте попробуем.

const toSlug = input => R.compose(
  R.split(' ')
)(input)

Как мы видим, код работает также, как и до этого. Давайте добавим остальные функции.

const toSlug = input => R.compose(
  encodeURIComponent,
  R.join('-'),
  R.map(R.toLower),
  R.split(' ')
)(input)

Как это работает? Мы применяем R.compose на input. Он идет последним аргументом и дальше вызываются все функции по очереди с права на лево. Так как все функции каррируются, то результат в следующую функцию передается автоматически.

Мы избавились от вложенности и обязательного прокидывания аргументов. Также наша функция toSlug, теперь избавилась от 4х ненужных переменных.

Единственное, что нужно помнить, это то, что R.compose тоже каррируется, поэтому этот код можно еще упростить, убрав аргумент.

const toSlug = R.compose(
  encodeURIComponent,
  R.join('-'),
  R.map(R.toLower),
  R.split(' ')
)

Если у вас возникли какие-то вопросы или комментарии, пишите их прямо под этим видео.

Только зарегистрированные пользователи могут оставлять комментарии.  Войдите, пожалуйста.
Mad Bad
19 дней назад
Да ну нет! Не задаём мы функции R.compose последним аргументом input! Ну чего людей путать то. Несколько раз это прозвучало. И это неправильно. Input передается как аргумент функции, которая возращается по результату работы R.compose.
monsterlessons
19 дней назад
Вы абсолютно правы, и это печально. Так как, имхо, оно должно было бы работать как обычная каррируемая функция.
Mad Bad
19 дней назад
Но в целом серия уроков по Ramda гиперинтересная. Не знаю насколько это Вас заинтересует, но лично мне хотелось бы увидеть практическое применение Ramda скажем c Redux. Или что то из еще более функциоального - про ramda-fantasy
monsterlessons
18 дней назад
Как раз Ramda с Redux я полностью разбирал в https://monsterlessons.com/project/series/internet-magazin-na-reactredux Ramda-fantasy/Sanctuary и тп мне не зашли. Я не сильно вижу полезность их применения в js. Это какой-то вариант недоделанного haskell, который все сильно переусложняет.
Valerii Kuzivanov
1год назад назад
Да уж, после ооп как-то всё вверх тормашками кажется))
monsterlessons
1год назад назад
В этом есть смысл. Нужно просто читать справа - налево. То есть у нас есть данные, мы применяем трансформации и в конце присваиваем значение в переменную const result = R.compose(трансформации)(начальные данные). Если вам не нравится, что все справа налево и вы хотите наоборот вы можете использовать pipe. Это тоже самое но в обратном порядке const result = R.pipe(трансформации_слева_направо)(начальные данные)
Denis
1год назад назад
R.pipe выглядит привычнее, все привыкли читать слева направо и сверху вниз, так почему вы используете R.compose? Или я упускаю какой-то аспект функционального программирования?
monsterlessons
1год назад назад
Нет никакого аспекта. R.pipe внутри себя вызывает reverse на аргументы, переставляя функции в перевернутом порядке и потом вызывает R.compose. Используйте что вам нравится. Результат одинаковый. Главное, чтобы в проекте был один код стайл - либо pipe, либо compose.
yarovoy
1год назад назад
Добрый вечер, подскажите пожалуйста, изучая этот урок хотел поэксперементировать на реальном примере const obj = { uuid: 'uuid-val', device: 'device-val' }; Object.keys(obj).map(function(key) { return [key, obj[key]].map(encodeURIComponent).join("="); }).join("&") Результат - uuid=uuid-val&device=device-val Попытался сделать эту запись с помощью compose const joinParams = obj => R.compose( encodeURIComponent, R.join("="), R.map, - на этапе этого map не получается определить ключ значение, что я должен был делать на этом моменте подскажите плз? R.keys )(obj); const params = joinParams(obj);
monsterlessons
1год назад назад
R.compose( encodeURIComponent, R.join('&'), R.map(R.join('=')), R.toPairs )(obj) R.keys тут не очень подходит, так как в цепочке мы не можем простым способом создать дополнительную переменную, где мы что-то сохраним. Поэтому 1. Обьект превращаем в пары ключ значение 2. Проходимся map и джоиним каждую пару 3. Джойним все елементы массива.
yarovoy
1год назад назад
Спасибо большое! Нужно углубленней ramda читать, не хватило понимания какой метод применть
monsterlessons
1год назад назад
Это да. Я в своем время потратил 2 недели времени, чтобы пройтись по всем методам Ramda и изучить зачем они нужны.
yarovoy
1год назад назад
Ага, без этого никак, спасибо!