Помеченные данные – это одна из ключевых тем нашего курса, поэтому крайне важно разобраться с этим уроком и понять всё, что здесь объясняется.
Давайте посмотрим на реализацию (имплементацию), которую мы сделали до этого и разберём те недостатки, которые она в себе несёт.
const cards = l(
cons('Костяная кочерга гробницы', () => 6)
);
const game = make(cards);
// inside ...
const card = random(cards);
const cardName = pairs.car(card);
const damage = pairs.cdr(card)();
const newHealth = health2 - damage;
Достаём имя карты, damage
и применяем его к здоровью игрока и после этого получаем новое здоровье.
С первого взгляда видно, то что здесь у нас нет никакой абстракции и мы работаем напрямую с парами, что не очень здорово. Например, если наши карты станут другие, они станут сильно сложнее и будут содержать больше информации, нам придётся полностью переписывать игровую логику и использовать здесь какую-то другую структуру. Естественно нам нужна какая-то абстракция.
// percentCard.js
import { cons, car, cdr } from 'hexlet-pairs';
export const make = (name, percent) =>
cons(name, percent);
export const getName = (card) => car(card);
export const damage = (card, health) =>
Math.round(health * (cdr(card) / 100));
Создаём отдельный файл под реализацию карты конкретного типа. Например, у нас есть простая карта и процентная карта, которая снимает урон в зависимости от того какой процент ей сказали снимать.
make
– это конструктор , который возвращает пару, getName
извлекает имя (это car
в паре) и damage
извлекает cdr
из карты и применяет его к переданному здоровью (второй параметр). Это классическая абстракция.
simpleCard.make('Ошарашивающие шорты равновесия', 7);
const cardName = simpleCard.getName(card);
const damage = simpleCard.damage(card);
// or
percentCard.make('Фаланговая знатность утешения', 80);
const cardName = percentCard.getName(card);
const damage = percentCard.damage(card, health2);
Используем make
, чтобы создать карту, а getName
и damage
для извлечения того, что нам нужно.
Есть один маленький нюанс. В одном случае функции damage
нужно здоровье, в другом нет. В JavaScript функции работают так, что если у нас есть второй параметр и он не всегда обязательный, то его можно не передавать. Это будет важно в дальнейшем когда мы сведём всё в один интерфейс.
Мы берём какую-то карту и после этого мы должны извлечь её имя, но мы не знаем её тип. Это слово само сюда ворвалось естественным образом, то есть нам нужно знать с какой картой мы работаем для того, чтобы понимать какой модуль к ней применить.
const iter = (...) => {
// some code ...
const card = random(cards);
const cardName = ?.getName(card);
const damage = ?.damage(card, health2);
Функция getName
у нас одинаковая (это могло быть не так), но функция damage
разная и она зависит от того, с каким типом мы сейчас работаем.
Из этого примера видно, что когда у нас появляется многообразие типов, с которыми мы хотим работать одинаковым способом, то обычный подход перестаёт работать целиком и полностью, потому что мы теперь не знаем, что здесь находится. И для этого нам нужно их как-то различать. Мы должны точно знать, что сейчас нам пришло, чтобы вызвать соответствующие функции.
Есть методика, которая называется помеченные данные. Всё сводится к тому, что мы берём какие-то данные и помечаем их какой-то специальной меткой, которая называется метка типа. Именно она будет определять с чем мы работаем.
Как мы будем это использовать?
У нас появляется модуль type
, который в себе содержит несколько функций: attach
и contents
. Работают они крайне просто.
import { cons, car, cdr } from 'hexlet-pairs';
import { attach, contents } from './type';
export const make = (name, percent) =>
attach('PercentCard', cons(name, percent));
export const getName = (self) => car(contents(self));
Теперь в нашей функции make
мы используем attach
, который первым параметром принимает метку – это имя типа, а вторым параметром те данные, которые мы там конструируем.
У функции getName
теперь тоже есть отличие. Мы используем функцию contents
, которая принимает на вход нашу карту и именуем её self
. Мы обозначаем её так, потому что она ссылается сама на себя. self
– это помеченная карта, поэтому сначала нужно из неё извлечь контент и делается это с помощью contents
.
Отсюда видно, что наш модуль type
построен очень грамотно с точки зрения модульности. Мы можем его применять абсолютно к любым данным и нам вообще не важна их структура.
import { cons, car, cdr } from 'hexlet-pairs';
export const attach = (tag, data) =>
cons(tag, data);
export const typeTag = (taggedData) =>
car(taggedData);
export const contents = (taggedData) =>
cdr(taggedData);
attach
представляет собой ещё одну пару поверх данных, где первый параметр – это метка типа, а второй – данные, с которыми мы работаем. Функция contents
возвращает сами данные, а функция typeTag
возвращает имя тега (это нужно для того, чтобы узнать тип сущности, с которой мы работаем).
С использованием отдельных типов карт наши тесты будут выглядеть так:
import * as simpleCard from './simpleCard.js';
import * as percentCard from './percentCard.js';
let cardIndex = 2;
const cards = l(
simpleCard.make('Ошарашивающие шорты', 7),
percentCard.make('Фаланговая знатность', 80)
);
const game = make(cards, (c) => {
cardIndex = cardIndex === 1 ? 2 : 1;
return get(cardIndex, c);
});
const log = game('John', 'Ada');
assert.equal(length(log), 5);
Во-первых, карты теперь нужно импортировать отдельно, они у нас содержатся в собственных модулях. Имена функций у них пересекаются, что достаточно важно. Нет смысла делать разные имена когда подразумевается одна и та же работа у одинакового типа данных. В следующих уроках мы увидим это особенно явно и чётко, почему их нужно делать одинаковыми. По этой причине мы импортируем модули как имена, а не загружаем функции напрямую из-за совпадения имён.
Теперь создание карт выглядит следующим образом: вызываем make
с приставкой конкретного модуля.
simpleCard.make('Ошарашивающие шорты', 7),
percentCard.make('Фаланговая знатность', 80)
В данном курсе, для простоты, мы работаем с двумя картами, но их может быть больше и они могут быть гораздо сложнее. Наша система уже позволяет расширять эту функциональность без проблем.
Работать с парами напрямую подразумевая, что это карты – не очень правильно, потому что получается достаточно хрупкий код, который легко будет меняться, если поменяется реализация карт.
Иногда вызываемый код зависит от типа и если у вас есть множество подобных типов, которые решают одну задачу и могут взаимозаменяться, то нужен какой-то механизм выбора. Поэтому вводятся теги, которые позволяют нам определять с каким типом данных мы сейчас работаем.
Вам ответят команда поддержки Хекслета или другие студенты.
Выделите текст, нажмите ctrl + enter и отправьте его нам. В течение нескольких дней мы исправим ошибку или улучшим формулировку.
Загляните в раздел «Обсуждение»:
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.
Наши выпускники работают в компаниях:
Зарегистрируйтесь или войдите в свой аккаунт