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

В моей предыдущей статье мы подсчитали калории еды, которую несут эльфы. Мы использовали только «Ramda.js и старались максимально придерживаться бесточечного стиля. Я и не подозревал, насколько это было легко по сравнению со вторым днем! Хотите узнать, почему? Пожалуйста, продолжайте читать.

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

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);

И далее отдельные строки в массивы букв:

import R from "ramda";

import data from "./data.txt";

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

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

import R from "ramda";

import data from "./data.txt";

const MOVE_VALUES = {
  A: 1,
  B: 2,
  C: 3,
  X: 1,
  Y: 2,
  Z: 3
};

R.pipe(
  R.split("\n"),
  R.map(R.split(" ")),
  R.map(R.map((move) => R.prop(move, MOVE_VALUES))),
  console.log
)(data);

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

Теперь самое сложное: подсчет очков. Это будет большое событие (вспомните шутки Майкла Скотта из «Офиса»):

import R from "ramda";

import data from "./data.txt";

const MOVE_VALUES = {
  A: 1,
  B: 2,
  C: 3,
  X: 1,
  Y: 2,
  Z: 3
};

const moduloThree = R.flip(R.uncurryN(2, R.modulo))(3);
const applyModuloToIndex = (index) => R.over(R.lensIndex(index), moduloThree);

const comparatorTransformFunctions = [
  R.identity,
  applyModuloToIndex(0),
  applyModuloToIndex(1),
  R.map(moduloThree)
];

const compareMoves = (difference) =>
  R.anyPass(
    R.map((fn) => R.pipe(fn, R.apply(R.subtract), R.equals(difference)))(
      comparatorTransformFunctions
    )
  );

const [
  isFirstMoveHigher,
  isSecondMoveHigher,
  areMovesEqual
] = R.map(compareMoves, [1, -1, 0]);

const evaluateMoves = R.cond([
  [areMovesEqual, R.always(3)],
  [isSecondMoveHigher, R.always(6)],
  [isFirstMoveHigher, R.always(0)]
]);

R.pipe(
  R.split("\n"),
  R.map(R.split(" ")),
  R.map(R.map((move) => R.prop(move, MOVE_VALUES))),
  R.map(R.pipe(R.juxt([R.last, evaluateMoves]), R.sum)),
  console.log
)(data);

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

R.map(R.pipe(R.juxt([R.last, evaluateMoves]), R.sum)),

Вызовы map и pipe уже должны быть очевидны — мы используем map для перебора всех пар ходов, а pipe — потому что собираемся вызывать более одной функции одновременно. Давайте посмотрим, что происходит внутри вызова pipe. Во-первых, это juxt. Это действительно очень удобная функция — она работает как перевернутая карта — вместо того, чтобы принимать одну функцию и список значений, а затем вызывать функцию при переборе значений, juxt принимает список функций и одно значение. и возвращает массив той же длины, что и массив функций, причем каждая функция вызывается с одним и тем же (одним) значением.

Чем это полезно? Давайте посмотрим на требования. Для каждой пары ходов нам нужно вычислить два значения: первое — это значение нашего хода, а второе — значение, основанное на результате одного раунда. Итак, на основе одного ввода нам нужно иметь два значения — juxt идеально подходит, вызывая две функции над одним и тем же значением. Первая предоставленная функция проста — это последний член массива moves — поскольку мы получаем очки за выбранный нами ход. Второй называется evaluateMoves:

const evaluateMoves = R.cond([
  [areMovesEqual, R.always(3)],
  [isSecondMoveHigher, R.always(6)],
  [isFirstMoveHigher, R.always(0)]
]);

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

  • cond работает почти так же, как оператор switch-case; он принимает массив массивов функций; если первая функция в массиве возвращает значение truthy, вызывается вторая функция.
  • always всегда (sic) возвращает одно и то же значение — его начальный аргумент.

Мы ясно видим, что если ходы равны, счет равен 3. И так далее. Посмотрим, как будут сравниваться ходы. Начнем с верхней части полного списка. Чтобы правильно сравнивать ходы, нам иногда нужно будет выполнять операцию modulo. Подготовим для этого функцию:

const moduloThree = R.flip(R.uncurryN(2, R.modulo))(3);

Что здесь случилось? Оригинальная функция Ramda modulo на самом деле оперирует данными. Это означает, что мы не можем сделать что-то вроде:

const moduloThree = R.modulo(3);

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

const moduloThree = (dividend) => R.modulo(dividend, 3);

В качестве альтернативы мы можем изменить порядок аргументов (для чего мы используем функцию flip), но сначала нам нужно uncurry изменить исходную функцию modulo, для которой также необходимо указать ее арность (равную 2). ). После этого, так как flip возвращает каррированную функцию, мы можем сразу вызвать ее с нужным нам делителем (коим является три).

Далее у нас есть еще одна полезная функция:

onst applyModuloToIndex = (index) => R.over(R.lensIndex(index), moduloThree);

Что он делает, так это применяет нашу функцию moduloThree к выбранному индексу в массиве. Мы используем функцию over, которая вызывает функцию, используя предоставленную линзу, а lensIndex создает линзу на основе предоставленного индекса. Давайте посмотрим, как это используется:

const comparatorTransformFunctions = [
  R.identity,
  applyModuloToIndex(0),
  applyModuloToIndex(1),
  R.map(moduloThree)
];

Здесь мы создаем массив всех комбинаций функций, которые нам понадобятся для правильного сравнения ходов. Зачем нам это нужно? Камень имеет значение 1, Ножницы имеет значение 3 и Камень > лучше, чем Ножницы, верно? Так что мы должны удовлетворить это. Мы можем преобразовать оба значения по модулю трех и проверить, какое из них больше, но это не будет охватывать сценарий Бумага и Ножницы. Что мы можем сделать, так это проверить по разнице одного в четырех разных сценариях — без модулей, оба значения с применением модуля и с модулем, примененным к любому из них. Вот почему нам нужны четыре функции преобразования. Давайте посмотрим, как они используются:

const compareMoves = (difference) =>
  R.anyPass(
    R.map((fn) => R.pipe(fn, R.apply(R.subtract), R.equals(difference)))(
      comparatorTransformFunctions
    )
  );

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

const [
  isFirstMoveHigher,
  isSecondMoveHigher,
  areMovesEqual
] = R.map(compareMoves, [1, -1, 0]);

Это довольно просто — если разница равна 1, выигрывает первый ход из пары; если -1 — выигрывает второй. Если разница равна нулю, то ничья. Теперь мы можем использовать их в функции evaluateMoves, тем самым завершая этот шаг.

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

R.map(R.pipe(R.juxt([R.last, evaluateMoves]), R.sum)),

мы рассчитали значения хода и результата и сложили их. Но теперь у нас есть очень длинный массив очков для отдельных ходов! Давайте быстро суммируем их:

import R from "ramda";

import data from "./data.txt";

const MOVE_VALUES = {
  A: 1,
  B: 2,
  C: 3,
  X: 1,
  Y: 2,
  Z: 3
};

const moduloThree = R.flip(R.uncurryN(2, R.modulo))(3);
const applyModuloToIndex = (index) => R.over(R.lensIndex(index), moduloThree);

const comparatorTransformFunctions = [
  R.identity,
  applyModuloToIndex(0),
  applyModuloToIndex(1),
  R.map(moduloThree)
];

const compareMoves = (difference) =>
  R.anyPass(
    R.map((fn) => R.pipe(fn, R.apply(R.subtract), R.equals(difference)))(
      comparatorTransformFunctions
    )
  );

const [
  isFirstMoveHigher,
  isSecondMoveHigher,
  areMovesEqual
] = R.map(compareMoves, [1, -1, 0]);

const evaluateMoves = R.cond([
  [areMovesEqual, R.always(3)],
  [isSecondMoveHigher, R.always(6)],
  [isFirstMoveHigher, R.always(0)]
]);

R.pipe(
  R.split("\n"),
  R.map(R.split(" ")),
  R.map(R.map((move) => R.prop(move, MOVE_VALUES))),
  R.map(R.pipe(R.juxt([R.last, evaluateMoves]), R.sum)),
  R.sum,
  console.log
)(data);

Вот и все! На этот раз код (не считая ответа на День 2 Пришествия кода 2022 года) не похож на рождественскую елку, но он точно набит вкусностями, какими я бы хотел, чтобы мои подарки были!

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