Полный список актуальных статей:
В моей предыдущей статье мы подсчитали калории еды, которую несут эльфы. Мы использовали только «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. Следуйте за мной для статей, охватывающих следующие дни! Также — это только первая половина решения — поделитесь своим в комментариях!