Я люблю Redux. Подумаешь, правда? Многие люди. А если серьезно, Redux предоставляет отличную систему для управления состоянием в сложных приложениях JavaScript. Не только это, но и первая библиотека, подобная Flux, которую я действительно понял.

Еще люблю Рамду. Я определенно не единственный, кто там есть, хотя он не совсем на том же уровне популярности, что и Redux. Это позор, потому что две библиотеки могут действительно хорошо работать вместе. Хотя они служат двум очень разным целям, их конечная цель одна - помочь вам написать более функциональные и декларативные ™ приложения на JavaScript.

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

Обзор функций селектора

Если вы не знакомы с функциями селектора в Redux, вот основная идея: вместо того, чтобы ваши компоненты представления обращались непосредственно к дереву состояний, вы определяете функции, которые получают эти данные за вас. Вы можете думать о них как о публичном API чтения для вашего штата. Они не являются строго обязательными для использования Redux - они скорее условны, чем требования. В простейших приложениях они могут вам не понадобиться (в простейших приложениях Redux может вообще не понадобиться), но они полезны, если ваше состояние имеет сложную структуру или вам нужно вычислить какие-то производные данные из него.

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

Получение свойства из дерева состояний

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

Получение данных из собственности

Подобно первому примеру, только теперь вместо того, чтобы читать свойство напрямую, мы вычисляем значение на его основе - в данном случае, независимо от того, установлено оно или нет. Вычисление логических значений на основе свойств состояния - распространенный вариант использования селекторов Redux. Конечно, возможны и другие виды расчетов.

Получение данных из списка элементов

Предполагая, что каждый объект-элемент имеет свойство count, этот селектор использует array.reduce для суммирования всех значений вместе. Обратите внимание, что элементы хранятся в объекте с именем byId, где каждый ключ объекта является идентификатором, а значение - самим элементом, а не хранением их в массиве. Этот организационный метод иногда называют нормализацией состояния и рекомендуется в документации Redux, а также Дэном Абрамовым, первоначальным создателем Redux. Такая организация данных (в сочетании с массивом ids для сохранения порядка) позволяет упростить поиск и обновление отдельных элементов.

Недостатком этого подхода является то, что нам нужно пройти некоторую гимнастику, если нам когда-либо понадобится доступ к данным в виде массива, как в этом примере. Поскольку простой объект не имеет reduce метода, мы сначала вызываем Object.keys для объекта, чтобы получить массив идентификаторов, и вместо этого работаем с ним. Немного некрасиво, но работает.

Эти примеры довольно общие, но они демонстрируют виды селекторов, которые вы можете написать в приложении Redux. Давайте посмотрим их все вместе, на этот раз экспортируя их, чтобы их можно было использовать в наших компонентах. (Мы используем синтаксис ES2015 export, для использования которого вам может потребоваться транспилятор, такой как Babel.)

Если мы используем синтаксис export, мы, вероятно, также можем использовать стрелочные функции ES2015, чтобы сделать эти селекторы еще лучше:

Эти неявные возвраты делают функции красивыми и лаконичными. Неплохо! Обратите внимание, что я заменил вызов Object.keys на Object.values, который, как следует из названия, возвращает массив значений свойства values ​​, а не ключей, что делает менее громоздким получение данные, которые нам действительно нужны. (Object.values - это функция ES2017, и вам понадобится полифилл, чтобы использовать ее в средах, которые ее не поддерживают. Использование преобразований Babel недостаточно, потому что это встроенный метод - вам понадобится что-то вроде babel- пойлфилл .)

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

Что важно знать о Рамде

Ramda позиционирует себя как «Практическая функциональная библиотека для программистов JavaScript». Ваша реакция на это описание может быть одной из следующих:

  1. Прохладный!
  2. Для этого у меня уже есть Lodash.
  3. У меня есть свои стрелочные функции и методы массива, и мне больше ничего не нужно, большое спасибо.

Прежде чем я попытаюсь убедить вас присоединиться к лагерю №1, позвольте мне начать с заявления об отказе от ответственности. Вам не нужен Ramda, как и React для создания компонентов представления. Добавление сторонней библиотеки - это решение, которое вы должны тщательно взвесить, учитывая такие затраты, как увеличение размера вашего приложения, время для новых членов команды, чтобы набраться опыта и т. Д. И это правда, что вы можете выполнить все, что делает Ramda. с ванильным JavaScript. Но, как и любая сторонняя библиотека, Ramda может помочь вам перестать беспокоиться о рутинных деталях реализации и вместо этого сосредоточиться на уникальной логике вашего приложения. Это мой ответ на реакцию №3 выше.

Теперь поговорим о №2. Почему вы должны выбрать Рамду вместо Лодаша? Lodash - отличная библиотека, и я успешно использовал ее в нескольких проектах. Но давайте посмотрим, что отличает Ramda от других, и для этого я процитирую домашнюю страницу Ramda:

Основные отличительные особенности Ramda:

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

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

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

Эти последние два момента - это то, что я хочу подчеркнуть при работе с селекторами Redux.

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

Функции Ramda каррируются автоматически. Что такое карри? Звучит вкусно! По сути, каррирование - это идея преобразования функции, которая принимает N аргументов, в последовательность из N функций, каждая из которых принимает по одному аргументу.

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

Но карри-форма добавления будет выглядеть так:

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

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

Как оказалось, Ramda выполняет своего рода волшебное каррирование (ням!), Которое позволяет использовать его функции как в карри, так и без каррирования. Другими словами, если у вас есть все аргументы для функции одновременно, вы можете вызвать ее как обычную функцию (например, add(1, 2)), вместо того, чтобы всегда использовать каррированную форму (например, add(1)(2)), что немного громоздко, когда вы есть все аргументы.

Если вам интересно, у Ramda есть add функция.

Данные Последние

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

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

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

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

Неплохо. Передав в map именованную функцию, которая описывает происходящее преобразование, легче увидеть, что происходит. Но хотя функция double может использоваться повторно, функция map - нет. Вам нужно вызвать map для каждого массива, к которому вы хотите применить это преобразование.

Ramda также имеет map функцию, но она имеет существенное отличие от встроенной функции map, а также от функции map Lodash: вы сначала передаете функцию преобразования, а затем массив, с которым нужно работать.

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

Видите, что мы только что там сделали? Мы создали новую функцию, которая принимает массив и возвращает новый с удвоением каждого элемента. И причина, по которой мы могли это сделать, заключается в том, что функция map принимает массив - данные, с которыми нужно работать, - последней. Данные - это часть, которая, скорее всего, изменится, поэтому, поместив ее в последнюю очередь, мы сможем гораздо легче создавать выразительные функции, которые можно повторно использовать с несколькими частями данных. Когда данные предоставляются первыми, как во встроенной функции map и map Lodash, мы с большей вероятностью напишем кучу одноразовых функций, которые менее выразительны, потому что данные постоянно меняются.

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

Теперь давайте посмотрим, как Ramda помогает нам лучше писать селекторы Redux.

Написание селекторов с помощью Ramda

Вернемся к примерам селекторов, которые мы написали ранее, и перепишем их с помощью Ramda.

getUserName

Начнем с самого простого:

Для этого нам нужно получить вложенные свойства от объекта state. У Ramda есть для этого удобная функция path. Первый параметр - это массив, определяющий путь, а второй - объект, на котором нужно найти свойство.

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

Одним из хороших преимуществ использования path является то, что если какая-либо часть пути не определена, она вернет undefined без выдачи ошибки. Конечно, если вы правильно настроили хранилище Redux, объект user никогда не должен быть неопределенным, но это все же хороший способ избежать этой ужасной ошибки.

Помните, как каррированы функции Ramda? Давайте вместо этого воспользуемся каррированной формой path:

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

Бум. Вот и все. Попробуй; вы увидите, что это работает точно так же.

Обратите внимание, что мы определили функцию, даже не упомянув данные, с которыми она работает. Эта техника известна как бесточечный стиль или неявное программирование. Идея оперировать данными без выявления аргументов, которые используются, может показаться странной - это определенно показалось мне, когда я впервые узнал об этом. В конце статьи я расскажу о некоторых преимуществах безточечного стиля. На данный момент, по крайней мере, мы можем видеть, что это дает более краткое определение функции, и кто не любит сокращать набор текста?

Автоматическое каррирование и порядок параметров Ramda - вот что делает возможным стиль без точек.

isLoggedIn

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

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

С Ramda мы, конечно, сможем!

Один из способов переписывания таких функций, который я нашел, - это спросить себя: что я пытаюсь извлечь из данных? В этом случае, как мне определить вход в систему на основе состояния? Согласно этой функции, пользователь входит в систему, когда путь user.id к объекту состояния не равен нулю или не определен. Следовательно, если бы мы могли определить функцию pathIsNotNullOrUndefined, которой мы могли бы передать путь и объект состояния, мы были бы на пути к Функциональному городу!

Итак, мы ищем функцию, которую можно было бы вызвать так:

Основываясь на имени функции, мы можем видеть, что есть две части: поиск пути и определение, не является ли значение нулевым или неопределенным. Похоже, место для дополнительных функций. Мы уже знаем, что у нас есть path, поэтому давайте рассмотрим первую часть, а затем посмотрим, как мы можем соединить их вместе.

Мы могли бы определить наш собственный isNotNullOrUndefined (или isSet, или как вы его называете) без особых трудностей, но ради этой статьи давайте посмотрим, что Ramda может нам предложить.

Иногда бывает трудно найти подходящую функцию Ramda, потому что их имена основаны на функциональных языках, таких как Haskell, и менее распространены в мире JavaScript. Но оказалось, что у Ramda есть функция с именем isNil, которая возвращает true, если входное значение равно null или undefined.

Большой! Теперь все, что нам нужно сделать, это отрицать это. Мы могли бы сделать это так:

Или мы могли бы пойти еще дальше, используя функции Ramda. Ты угадал! Существует not функция, которая в основном делает то же самое, что и !:

Вторая версия кажется глупой, но теперь мы не используем только функции, так что ура. Но мы можем добиться большего. Теперь, когда вы почувствовали вкус бесточечного стиля, вы, надеюсь, начнете искать возможности для рефакторинга под этот стиль. Можем ли мы определить функцию, которая вообще не ссылается на val?

да. Да мы можем. Но вместо использования not мы будем использовать другую функцию с именем complement. В то время как not немедленно возвращает дополнение переданного ему значения (то, что вы получили бы, поставив перед ним !), complement принимает функцию и возвращает новый функция, которая при вызове возвращает дополнение того, что возвращает исходная функция.

Если в этом нет смысла, посмотрите на это в действии, чтобы прояснить ситуацию:

Большой! Итак, теперь у нас есть функция isNotNil, написанная в этом приятном, безупречном стиле для хорошей меры.

А как это совместить с path? Сначала попробуем наивный подход:

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

Теперь это карри. Красивый. Но можем ли мы избавиться от этого надоедливого параметра state и сделать его еще красивее?

Здесь нам может помочь compose функция Ramda. Концепция функциональной композиции не уникальна для Ramda; это фундаментальная часть функционального программирования, уходящая своими корнями в математику. (Примечание: чем глубже вы углубитесь в функциональное программирование, тем больше будет казаться, что вы занимаетесь математикой. Я полагаю, это потому, что вы используете функции в прямом смысле этого слова, как функции в алгебре. Видите? Все эти годы математики действительно оказались полезными!)

Если вы когда-либо использовали функцию flow Lodash, вы использовали композицию функций, хотя в Ramda функции вызываются справа налево, а не слева направо, что типично для большинства функциональных языков. Например, используя compose, мы можем переписать указанную выше версию pathIsNotNil следующим образом:

В этой составной функции мы передаем аргументы path и state самой правой функции, R.path первой. Возвращаемое значение этой функции затем передается следующей самой правой функции - в данном случае isNotNil - и так далее, пока мы не дойдем до конца функций и не получим окончательное возвращаемое значение. Здесь важно помнить, что самая правая функция может принимать любое количество аргументов, но остальные должны принимать только один.

Если композиция с написанием справа налево кажется вам странной, подумайте об этом так: если бы функции были вложены друг в друга, как у нас изначально, какая функция была бы оценена первой? Самый внутренний, или, другими словами, самый правый. В качестве альтернативы вы можете использовать pipe функцию Ramda, которая является версией compose для письма слева направо, но я рекомендую вам попытаться привыкнуть к композиции с письмом справа налево, потому что так обычно и делается.

Теперь можно избавиться от параметра state? Учитывая то, что мы знаем о работе функций Ramda, мы можем попробовать следующее:

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

Но еще не все потеряно! Функция path является каррированной, поэтому мы можем частично применить эту конкретную функцию с аргументом пути, в результате получится составная функция, которая принимает только один аргумент для начала, например:

Теперь, когда это сделано, мы можем удалить state, в результате чего получится последняя красивая версия pathIsNotNil:

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

С помощью Ramda curry function мы теперь взяли скомпонованную функцию и превратили ее в каррированную функцию, которая принимает аргументы по одному. Сначала он выбирает путь, а затем объект данных. Другими словами, это именно та функция, которая, как мы говорили, нам нужна в начале.

Итак, теперь мы знаем, что можем сделать это таким образом. Вопрос в том, должны ли мы. В конечном итоге решение остается за вами, но я думаю, что в данном случае мы можем зайти слишком далеко. Как вы уже могли догадаться, мне нравится стиль без точек, как и другим людям, но у него есть свои ограничения. Во-первых, мы создали дополнительный уровень абстракции в этой функции до такой степени, что не сразу понятно, какова ее сигнатура. Без хорошей документации вы, вероятно, не узнали бы, что эта функция принимает два аргумента, не углубляясь в отдельные функции внутри. Теперь, когда вы ближе познакомитесь с Ramda, возможно, вы поймете это быстрее. В конце статьи я расскажу больше о том, что такое знакомство, а не простота.

Но вот мысль, которая может стать хорошим эмпирическим правилом, когда дело доходит до бесточечного стиля: когда вы отрефакторились до точки, когда вы больше не упоминаете фактические данные, с которыми нужно работать, вероятно, будет хорошей идеей прекратить там. В примере pathIsNotNil есть два аргумента, но только один из них является фактическими данными, с которыми мы работаем. Мы удалили этот аргумент перед использованием curry, и, вероятно, на этом нам следует остановиться. Мы знаем, что в конечном итоге мы передадим туда объект состояния, но, оставив параметр пути, мы сохраняем ясность, что нужно функции. Я могу ошибаться здесь, но я думаю, что это правило помогает мне не увлечься.

Что бы вы ни решили сделать, теперь у нас есть pathIsNotNil функция, готовая для использования в нашем селекторе:

Ура! Мы сделали это! Более того, теперь у нас есть пара служебных функций (isNotNil и pathIsNotNil), которые почти наверняка будут полезны в другом месте приложения.

Попробуйте этот пример в Ramda REPL.

getTotalItemCount

Теперь наш самый сложный селектор getTotalItemCount. Вместо того, чтобы смотреть на одно значение состояния, он оперирует их списком. Вот что у нас сейчас есть:

Здесь происходит довольно много всего, и как только вы достигнете определенного уровня сложности, вы обнаружите, что есть несколько способов разбить проблему на функциональный стиль. Поэтому я объясню две разные возможности, как этого добиться. Это, конечно, не единственные способы сделать это и не обязательно лучшие. Кто-нибудь, более продвинутый в FP и Ramda, почти наверняка найдет более эффективный или разумный способ сделать это. Я призываю вас попробовать разные способы и посмотреть, сможете ли вы сделать это лучше.

Как и в последнем примере, давайте сформулируем нашу проблему, рассмотрев, что мы пытаемся узнать из данных. Отложив на мгновение всю работу, которую нам нужно проделать, чтобы добраться до соответствующей части дерева состояний, суть того, что мы пытаемся сделать, заключается в следующем: получить сумму свойства count каждого элемента.

Давайте создадим функцию sumCounts, которая принимает массив элементов и возвращает сумму свойств count каждого элемента. Вот первый удар с помощью ванильного JavaScript:

Теперь мы хотели бы переписать это, используя функции Ramda, в идеале без точек. Здесь мы разделимся на две разные возможности. Один из них полагается на использование map для преобразования элементов перед суммированием подсчетов, а другой использует reduce, чтобы сделать все это сразу.

Использование map

Использование map для преобразования данных в нужную нам форму, вероятно, является наиболее очевидным и простым способом сделать это. Это стало еще проще, потому что у Ramda есть sum функция, которая принимает массив чисел и возвращает, ну, сумму.

Помните, что compose передает наши аргументы (в данном случае массив элементов) самой правой функции, а затем возвращаемое значение этой функции следующей. Самая правая функция здесь - это map, которая применяет функцию, созданную prop('count').

Раньше мы не видели функцию prop, но она берет опору имени, которое вы даете ей, из объекта, который вы передаете ей в качестве второго аргумента. Он похож на path, но идет только на один уровень. Поскольку мы передаем здесь только первый аргумент, он создает функцию, которая принимает объект, из которого нужно извлечь опору. Таким образом, мы можем передать эту частично примененную функцию в map, который передаст ему каждый элемент массива. Таким образом, функция map сопоставляет каждый элемент с его свойством count, в результате чего получается массив чисел, который затем можно передать в sum.

Оказывается, использование map таким образом - для извлечения свойства из каждого объекта - настолько распространено, что Ramda предоставляет для него вспомогательную функцию под названием pluck. Использование pluck делает нашу функцию еще проще:

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

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

Использование reduce

Созданная нами версия сопоставления sumCounts выполнит свою работу. Но я хотел бы выяснить, как это сделать с помощью reduce. Если бы в моем проекте не было Ramda и мне приходилось использовать обычный JavaScript, я бы использовал reduce, как вы можете видеть выше. Почему? Потому что мне нравится избегать повторения цикла по массиву более одного раза, если это возможно. В версии map мы просматриваем массив один раз, чтобы сопоставить каждый элемент с его count, а затем еще раз для их суммирования. Это даже не считая работы, которую нам нужно будет сделать до этого, чтобы добраться до нужного нам набора элементов.

С reduce мы сможем выполнить оба этих шага за один раз. Возможно, это случай преждевременной оптимизации, особенно если она затрудняет понимание кода (хотя, опять же, я еще скажу о незнакомости в конце статьи). Это может быть даже промывка из-за того, что применяется много функций, что может снизить производительность. Но для аргументации и для лучшего понимания некоторых концепций Ramda давайте поработаем над reduce версией.

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

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

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

Разве так не лучше? Теперь мы приближаемся к описанию того, чего мы хотим от данных, а не того, как мы их получаем. Конечно, теперь нам нужно определить функцию addCount. Его простая извлеченная форма выглядит так:

Можем ли мы также определить эту функцию в терминах других функций? Посмотрим, чем Рамда может нам здесь помочь. Вы, наверное, думали, что никогда не воспользуетесь им, но давайте воспользуемся функцией Ramda add вместо синтаксиса сложения:

Хорошо, теперь вот где начинается самое интересное. Обратите внимание, как мы почти дошли до того, что передаем аргументы напрямую другой функции. Единственная разница в том, что вместо того, чтобы передавать item напрямую add, нам нужно извлечь свойство count из него. Если бы мы могли найти способ преобразовать этот аргумент до того, как он будет передан в add, мы могли бы полностью избавиться от аргументов и написать эту функцию в безточечном стиле. Это может показаться безумным, но оставайтесь со мной.

Мы уже видели функцию prop в map примере выше. Мы можем использовать ту же функцию здесь:

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

Как вы уже догадались - у Ramda есть для этого функция! Он называется useWith, и я не понимал его смысла, пока не работал над этой самой проблемой. Скорее всего, это будет казаться вам чуждым, если вы не привыкли к функциональному программированию. Это определенно повлияло на меня. Я постараюсь это объяснить.

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

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

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

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

Подведем итоги того, что мы здесь создали. Используя useWith, мы обернули функцию add некоторыми функциями-преобразователями. Первый аргумент, который соответствует общему количеству, передается функции identity и поэтому остается неизменным. Второй аргумент, который соответствует элементу в массиве, передается функции, созданной prop('count'), поэтому мы получаем свойство count от него. Затем эти два преобразованных аргумента передаются add. Результатом является функция, которую мы можем передать reduce, как мы и хотели:

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

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

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

Теперь, когда у нас есть sumCounts функция, нам нужно заставить ее работать с формой наших данных. Как обсуждалось выше, объект состояния выглядит так:

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

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

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

Подведение итогов: что мы сделали

Ух! Это был долгий путь, но мы его прошли, успешно переписав наши селекторы Redux в функциональном, компонованном и бессрочном виде. Давайте подведем итоги всего, что мы построили, собрав все это в один модуль.

Как видите, мы не только переписали наши селекторы более функциональным образом, мы также создали несколько вспомогательных функций в качестве строительных блоков для селекторов. Эти вспомогательные функции почти наверняка будут полезны в другом месте кода проекта, тогда как раньше все делалось с помощью одноразовых анонимных функций, которые нельзя было использовать повторно. Это одна из областей, в которой Ramda действительно выделяется: она дает вам множество полезных функций для начала, но автоматическое каррирование и порядок аргументов позволяют вам легко использовать эти функции и создавать еще более полезные функции для конкретных потребностей вашего приложения - функции который можно использовать в нескольких местах. Надеюсь, вы начинаете убеждаться в полезности Рамды.

Почему мы это сделали?

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

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

Я начну с того, что для других языков функции без точек являются полностью стандартными. В Haskell, например, большинство функций автоматически каррированы (что уместно, поскольку и Haskell, и каррирование названы в честь математика Haskell Curry), поэтому из этого естественным образом следует бесточечный стиль. Я знаю, что это делают другие само по себе - плохой аргумент. Я говорю об этом только для того, чтобы указать, что Рамда не изобретает здесь что-то новое, а опирается на десятилетия исследований и теории. Он может быть блестящим и горячим, но это не новость.

Так почему же он так популярен за пределами JavaScript Land? Признаюсь, мне иногда было трудно сформулировать преимущества, кроме как весело! Но эта статья от Рэнди Кулмана действительно помогла мне еще больше укрепить положение. Я уже упоминал, что стиль без точек может сделать ваши функции более лаконичными. Большой. Но реальный аргумент для меня в том, что это меняет то, как вы думаете о функциях. Вместо того, чтобы сосредотачиваться на самих данных, вы начинаете больше думать о преобразованиях, которые должны произойти, чтобы получить то, что вам нужно. Это не столько как, сколько что. Это действительно более декларативно. Я знаю, что слово декларативный часто используется в наши дни, но для меня бессмысленные функции действительно декларативны, потому что вы объявляете отношения между данными.

В конце концов, что такое функциональное программирование? Да, это программирование с использованием функций, но лучшее описание, которое я слышал относительно того, что отличает его от других стилей программирования, таково: данные отделены от функций, которые с ними работают. По сути, функциональное программирование - это использование функций для описания отношений между наборами данных. Кое-что из этого вы видите, например, при использовании React. Компонент React - это, по сути, функция, которая превращает данные приложения в данные, которые можно визуализировать.

Когда вы пишете функции в бессмысленном стиле, вы действительно учитываете отношения между наборами данных. Теперь, как мы видели с некоторыми из наших функций выше, стиль без точек не всегда возможен. И это не всегда желательно. Но по мере того, как вы начинаете привыкать искать возможности для беспоточного рефакторинга, как это делает Ramda, я надеюсь, вы обнаружите, что это меняет ваш подход к функциям в лучшую сторону.

И последнее замечание в заключение. Углубляться в область функционального программирования может быть неудобно. Конечно, время от времени это было для меня. Если вы измените код проекта, как в примерах, которые мы здесь рассмотрели, вы можете обнаружить, что коллеги (или даже вы сами) жалуются, что это делает код более сложным или трудным для понимания. На это я говорю следующее: не путайте простоту с привычностью. Сейчас это звучит банально, но это правда. Многие из нас считают FP трудным или сложным, потому что изначально мы были обучены совершенно другому стилю программирования. Я считаю, что если бы программист с самого начала обучался программированию на FP, он бы нашел объектно-ориентированное программирование странным и излишне сложным.

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

Первоначально опубликовано на www.garrettnay.com.