Если вы читали другие мои статьи, то, вероятно, знаете, что я немного одержим функциональным программированием. Если нет, то теперь да.
Сегодня я хочу поделиться с вами небольшим советом по написанию более чистого функционального кода.
Ах да, мы, наверное, все были там. Я говорю о страшной «проблеме мировой черепахи» в функциональном программировании. Это происходит, когда у нас есть значение, которое необходимо преобразовать с помощью (не малозначительной) серии вызовов функций.
«Черепахи на всем пути вниз…»
Это может выглядеть примерно так:
sort(compact(flatten(omit(map(filter(data, byMinSize(5)), transformRgbToHex), unusedProps))))
Уф, у меня уже голова болит. Таким образом, вы можете попытаться упростить грок, разбив его на части, что обычно приводит к чему-то вроде следующего:
const largeItems = filter(data, byMinSize(5)) const colorMappedItems = map(largeItems, transformRgbToHex) const slimmedDownItems = omit(colorMappedItems, unusedProps) const flatterList = flatten(slimmedDownItems) const reducedSet = compact(flatterList) const sortedStuff = sort(reducedSet)
Фу! Ну, теперь я просто устал!
Это легче следовать? Я даже не уверен! Может быть, но Боже, как многословно — и у кого есть время придумывать столько имен переменных? Ни один из этих примеров не идеален, это ясно видно. Я думаю, мы можем добиться большего.
Монады в помощь!!
Но подождите, что такое монада, спросите вы? Неважно. На самом деле, давайте никогда больше не будем говорить об этом. Кхм, а теперь…
С помощью простого инструмента, который есть в каждом наборе инструментов для JavaScript (на самом деле, он входит в комплект!) мы можем переписать этот код гораздо более выразительно и — осмелюсь сказать Это? — более читабельным способом. Этот инструмент, друзья мои, скромный Promise
.
Ты слышал меня. Мы собираемся решить эту проблему с помощью promises. Мы могли бы даже добавить щепотку async/await по вкусу.
Итак, без лишних слов:
const largeCoats = await Promise.resolve(data) .then(x => filter(x, byMinSize(5)) .then(x => map(x, transformRgbToHex) .then(x => omit(x, unusedProps) .then(flatten) .then(compact) .then(sort)
Это может показаться довольно нетрадиционным, но просто найдите минутку, чтобы вникнуть.
Теперь прочтите его вслух про себя. Продолжать. Никто не смотрит.
Это практически читается как рецепт.
«Чтобы сделать несколько больших слоев, сначала получите свои данные, затем отфильтруйте по минимальному размеру 5. Затем сопоставьте их, преобразовав RGB в шестнадцатеричный формат. Затем удалите неиспользуемый реквизит, сгладьте и уплотните его и, наконец, отсортируйте».
Я почти могу представить себе дорогую старую бабушку в ее любимом фартуке, решительно сгорбившуюся над клавиатурой…
Ладно, глупости в сторону, надеюсь, сегодня я показал вам что-то полезное. Мы можем легко превратить тот чудовищный код, с которого начали, в нечто более простое для понимания.
Однако становится еще лучше. Взгляните еще раз на первые два уродливых примера. Ладно, перестань искать, я смущаюсь — фу-у-у! Теперь, когда эти злодеяния снова свежи в вашей памяти, представьте, что вы пытаетесь реорганизовать любой из них. Возможно, вы поняли, что сделали некоторые операции в неправильном порядке. Или, может быть, вам нужно где-то вмешать еще одну операцию в микс.
«Я просто перенесу это сюда… скопирую это туда… ой, теперь я ввел неверный ввод в это… черт, несоответствие скобкам, опять…»
Мне страшно подумать. Что ж, тебе повезло. Используя этот подход Promise
, можно легко изменить порядок операций или вставить новую. Сходить с ума!
Очевидно, что в реальных условиях вам придется немного подумать о том, как упорядочить операции, но, как видите, мы избавились от большей части головной боли. Больше не нужно возиться с именами переменных или пытаться распутать однострочное чудовище.
Просто помните, что это работает не только с асинхронными вызовами (например, выборка по сети или что-то еще). Вы заметили, что я обернул Promise.resolves
поверх простого старого значения? Используйте его везде, где вам нужно выполнить серию операций, даже если источник данных уже не является асинхронным. Используя await
, мы превращаем его обратно в простое старое значение, и никто никогда не узнает. Это идеальное преступление!
На этом я оставлю вас с последним маленьким кусочком. Нечто, называемое краном. Что делает кран? Ну, очевидно, это позволяет вам подключиться к потоку данных, не прерывая его. Я часто использую это для отладки во время работы. Очень, очень удобно, когда вы работаете практически с любыми данными, протекающими по цепочке операций. Но, поскольку я занимаюсь отладкой, я не хочу устанавливать и импортировать зависимость, которую я все равно не буду поставлять с кодом. Итак, я держу этот небольшой фрагмент в своем поясе с инструментами. Он крошечный и довольно легко запоминается, так что вы можете просто набрать его на клавиатуре в любое время, когда возникнет такая необходимость.
Вот, кран!
const tap = f => g => (f(g), g)
Теперь все, что вам нужно сделать, это подключиться к вашей цепочке операций в любой точке, например:
// ... .then(transformSomething) // did it do what I think it should have? .then(tap(console.log)) .then(moreStuffIsHappening) // ...
Переместите кран вверх или вниз по цепочке или, черт возьми, коснитесь всего! Кто вам мешает?
Подводя итог, написание хорошего, удобочитаемого и удобного в сопровождении функционального кода не должно быть сложным. Не создавайте черепахи свои операции — связывайте их!
Дайте мне знать ваши мысли внизу в комментариях. Вы использовали эту технику раньше? Вам нравится подход?