Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Помеченные данные JS: Программирование, управляемое данными

Помеченные данные – это одна из ключевых тем нашего курса, поэтому крайне важно разобраться с этим уроком и понять всё, что здесь объясняется.

Недостатки имплементации

Давайте посмотрим на реализацию (имплементацию), которую мы сделали до этого и разберём те недостатки, которые она в себе несёт.

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 построен очень грамотно с точки зрения модульности. Мы можем его применять абсолютно к любым данным и нам вообще не важна их структура.

Устройство 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 и отправьте его нам. В течение нескольких дней мы исправим ошибку или улучшим формулировку.

Что-то не получается или материал кажется сложным?

Загляните в раздел «Обсуждение»:

  • задайте вопрос. Вы быстрее справитесь с трудностями и прокачаете навык постановки правильных вопросов, что пригодится и в учёбе, и в работе программистом;
  • расскажите о своих впечатлениях. Если курс слишком сложный, подробный отзыв поможет нам сделать его лучше;
  • изучите вопросы других учеников и ответы на них. Это база знаний, которой можно и нужно пользоваться.

Об обучении на Хекслете

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
900
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.

  • 130 курсов, 2000+ часов теории
  • 900 практических заданий в браузере
  • 360 000 студентов
Даю согласие на обработку персональных данных, соглашаюсь с «Политикой конфиденциальности» и «Условиями оказания услуг»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff

Используйте Хекслет по максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Даю согласие на обработку персональных данных, соглашаюсь с «Политикой конфиденциальности» и «Условиями оказания услуг»