Полный список актуальных статей:

Будучи большим поклонником парадигмы функционального программирования и используя ее везде, где это возможно, я решил попытаться решить задачу Advent of Code в этом году, используя только бесточечный код и (если возможно) только библиотеку Ramda. функции. Ниже моя попытка в День 1!

Во-первых, конечно, нам нужно импортировать Ramda (при условии, что она уже установлена):

import R from "ramda";

Ну, достаточно просто. Далее, для ввода вызова. Для простоты я сохраняю входные данные в виде файла txt рядом с моим index.js, поэтому я могу импортировать его следующим образом:

import R from "ramda";

import data from "./data.txt";

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

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

Во-первых, давайте создадим конвейер для функций, которые мы будем использовать. Поскольку мы собираемся где-то выводить результат, я сначала добавлю последний (который будет вызовом console.log) — таким образом, это поможет нам видеть промежуточные результаты по мере продвижения. :

import R from "ramda";

import data from "./data.txt";

R.pipe(
  console.log
)(data);

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

import R from "ramda";

import data from "./data.txt";

R.pipe(
  R.split("\n"),
  console.log
)(data);

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

import R from "ramda";

import data from "./data.txt";

R.pipe(
  R.split("\n"),
  R.map(parseInt),
  console.log
)(data);

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

import R from "ramda";

import data from "./data.txt";

R.pipe(
  R.split("\n"),
  R.map(parseInt),
  R.groupWith(R.identity),
  console.log
)(data);

Хорошо, давайте здесь помедленнее. Сложность этой линии немного выше, чем у предыдущих. groupWith (docs) поможет нам разделить наш массив на куски, каждый из которых представляет одного эльфа и его калорийность. Он принимает функцию сравнения, которая вызывается с двумя аргументами — текущим и следующим элементом итерируемого списка. Если функция сравнения возвращает значение truthy, текущий элемент добавляется к существующему фрагменту. Для значения falsy создается новый фрагмент и заполняется текущим элементом.

В нашем случае мы хотим начать новый кусок на границе между двумя эльфами, который в настоящее время представлен NaN (для интересующихся, средний хлеб Наан, согласно google.com, 262 калории). Мы можем использовать тот факт, что NaN (несмотря на его калорийность) является значением falsy. Все, что нам нужно, — это функция сравнения, которая примет первый аргумент и вернет его. Мы можем использовать функцию identity — все, что она делает, это возвращает переданный аргумент, который в нашем случае будет приведен к логическому значению.

Итак, теперь у нас есть массив массивов (каждый для одного Эльфа), но есть проблема — groupWith чанков включают в себя все элементы исходного массива. Нам нужно избавиться от NaNs (я больше не буду ездить на этом каламбуре Naan):

import R from "ramda";

import data from "./data.txt";

R.pipe(
  R.split("\n"),
  R.map(parseInt),
  R.groupWith(R.identity),
  R.map(R.filter(R.identity)),
  console.log
)(data);

На этом шаге мы сопоставляем всех наших эльфов, фильтруя их массивы, исключая NaNs. Для этого все, что нам нужно сделать, это передать функцию identity функции filter, как и в предыдущем шаге.

Теперь, когда у нас есть хороший, чистый список калорий (представленный в виде чисел) для каждого эльфа, мы можем приступить к их суммированию:

import R from "ramda";

import data from "./data.txt";

R.pipe(
  R.split("\n"),
  R.map(parseInt),
  R.groupWith(R.identity),
  R.map(R.filter(R.identity)),
  R.map(R.sum),
  console.log
)(data);

Мы могли бы использовать вызов reduce в качестве аргумента для map, но, к счастью, в этом нет необходимости — Ramda предоставляет полезную функцию sum.

Теперь у нас есть список калорий, которые несет каждый эльф! Последнее, что нужно сделать, это найти тот, который несет больше всего:

import R from "ramda";

import data from "./data.txt";

R.pipe(
  R.split("\n"),
  R.map(parseInt),
  R.groupWith(R.identity),
  R.map(R.filter(R.identity)),
  R.map(R.sum),
  R.apply(Math.max),
  console.log
)(data);

Почему нам нужно использовать apply здесь? Предыдущая функция с конвейером возвращает массив чисел, а Math.max принимает одиночные числа в качестве нескольких аргументов. apply делает именно это — вызывает предоставленную функцию по предоставленному списку аргументов в качестве расширенных.

Приведенный выше код не только дает правильный ответ на День 1 из 2022 Advent of Code, но также (на мой взгляд) довольно элегантен и, более того, немного похож на рождественскую елку!

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

import R from "ramda";

import data from "./data.txt";

const inversedSubtract = R.flip(R.uncurryN(2, R.subtract));

R.pipe(
  R.split("\n"),
  R.map(parseInt),
  R.groupWith(R.identity),
  R.map(R.filter(R.identity)),
  R.map(R.sum),
  R.sort(inversedSubtract),
  R.tap(R.pipe(R.head, console.log)),
  R.tap(R.pipe(R.take(3), R.sum, console.log))
)(data);

Внимательный читатель заметит, что есть пара отличий. Начнем сверху. Во-первых, мы добавили функцию inversedSubtract — инверсия необходима, чтобы изменить порядок аргументов на обратный без необходимости использования лямбда-функции. Чтобы добиться этого, нам нужно вызвать две функции Ramda — uncurryN снимает функцию, чтобы ее можно было перевернуть, и, ну, flip. Единственное, что здесь сложно, — это необходимость указать арность функции в качестве первого аргумента uncurryN — в данном случае это 2 (что является арностью функции subtract).

По-настоящему заинтересованный читатель также заметит, что в какой-то момент код основного конвейера изменился. Последняя строка, общая с предыдущим решением, — это та, в которой мы вычислили сумму калорий для каждого отдельного эльфа, используя R.map(R.sum). Мы можем использовать invertedSubtract сразу после этой строки, отсортировав весь массив в порядке убывания.

Зачем нам это нужно? Что ж, теперь нам нужно вычислить два значения из одного потока данных, а отсортированный массив — это собственно то место, где разветвляются наши будущие вычисления. Что я имею в виду? Можно подключиться к значению и использовать его для некоторых преобразований, позволяя передавать его дальше по конвейеру. Как это достигается? Неудивительно, что нам нужно использовать функцию tap. Функция в основном принимает один аргумент, который представляет собой функцию, которая будет выполняться со значением в том виде, в каком оно находится в текущем месте конвейера. В то же время значение передается дальше по конвейеру для использования в других преобразованиях. Если мы передаем функцию pipe функции tap, мы фактически создаем отдельный конвейер внутри конвейера.

Давайте посмотрим, как это работает. Мы называем

R.tap(R.pipe(R.head, console.log))

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

R.tap(R.pipe(R.take(3), R.sum, console.log))

в котором мы берем трех самых калорийных эльфов и суммируем их груз — что и является целью второй половины первого задания!

Рабочий пример можно найти в этой CodeSandbox. Следуйте за мной для статей, охватывающих следующие дни! В случае возникновения вопросов — смело задавайте их в комментариях.