JS: Программирование, управляемое данными
2026-02-26 17:18 Diff

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

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

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

Достаём имя карты, damage и применяем его к здоровью игрока и после этого получаем новое здоровье.

С первого взгляда видно, то что здесь у нас нет никакой абстракции и мы работаем напрямую с парами, что не очень здорово. Например, если наши карты станут другие, они станут сильно сложнее и будут содержать больше информации, нам придётся полностью переписывать игровую логику и использовать здесь какую-то другую структуру. Естественно нам нужна какая-то абстракция.

Прячем реализацию

Создаём отдельный файл под реализацию карты конкретного типа. Например, у нас есть простая карта и процентная карта, которая снимает урон в зависимости от того какой процент ей сказали снимать. make – это конструктор , который возвращает пару, getName извлекает имя (это car в паре) и damage извлекает cdr из карты и применяет его к переданному здоровью (второй параметр). Это классическая абстракция.

Используем абстракцию

Используем make, чтобы создать карту, а getName и damage для извлечения того, что нам нужно.

Есть один маленький нюанс. В одном случае функции damage нужно здоровье, в другом нет. В JavaScript функции работают так, что если у нас есть второй параметр и он не всегда обязательный, то его можно не передавать. Это будет важно в дальнейшем когда мы сведём всё в один интерфейс.

Проблема: не знаем тип карты

Мы берём какую-то карту и после этого мы должны извлечь её имя, но мы не знаем её тип. Это слово само сюда ворвалось естественным образом, то есть нам нужно знать с какой картой мы работаем для того, чтобы понимать какой модуль к ней применить.

Функция getName у нас одинаковая (это могло быть не так), но функция damage разная и она зависит от того, с каким типом мы сейчас работаем. Из этого примера видно, что когда у нас появляется многообразие типов, с которыми мы хотим работать одинаковым способом, то обычный подход перестаёт работать целиком и полностью, потому что мы теперь не знаем, что здесь находится. И для этого нам нужно их как-то различать. Мы должны точно знать, что сейчас нам пришло, чтобы вызвать соответствующие функции.

Решение: помеченные данные

Есть методика, которая называется помеченные данные. Всё сводится к тому, что мы берём какие-то данные и помечаем их какой-то специальной меткой, которая называется метка типа. Именно она будет определять с чем мы работаем.

Как мы будем это использовать? У нас появляется модуль type, который в себе содержит несколько функций: attach и contents. Работают они крайне просто.

Теперь в нашей функции make мы используем attach, который первым параметром принимает метку – это имя типа, а вторым параметром те данные, которые мы там конструируем. У функции getName теперь тоже есть отличие. Мы используем функцию contents, которая принимает на вход нашу карту и именуем её self. Мы обозначаем её так, потому что она ссылается сама на себя. self – это помеченная карта, поэтому сначала нужно из неё извлечь контент и делается это с помощью contents. Отсюда видно, что наш модуль type построен очень грамотно с точки зрения модульности. Мы можем его применять абсолютно к любым данным и нам вообще не важна их структура.

Устройство type внутри

attach представляет собой еще одну пару поверх данных, где первый параметр – это метка типа, а второй – данные, с которыми мы работаем. Функция contents возвращает сами данные, а функция typeTag возвращает имя тега (это нужно для того, чтобы узнать тип сущности, с которой мы работаем).

Тесты

С использованием отдельных типов карт наши тесты будут выглядеть так:

Во-первых, карты теперь нужно импортировать отдельно, они у нас содержатся в собственных модулях. Имена функций у них пересекаются, что достаточно важно. Нет смысла делать разные имена когда подразумевается одна и та же работа у одинакового типа данных. В следующих уроках мы увидим это особенно явно и чётко, почему их нужно делать одинаковыми. По этой причине мы импортируем модули как имена, а не загружаем функции напрямую из-за совпадения имён.

Теперь создание карт выглядит следующим образом: вызываем make с приставкой конкретного модуля.

В данном курсе, для простоты, мы работаем с двумя картами, но их может быть больше и они могут быть гораздо сложнее. Наша система уже позволяет расширять эту функциональность без проблем.

Итоги

  • Реализацию необходимо прятать
  • Иногда вызываемый код зависит от типа
  • Типы можно ввести через специальные метки
  • Типы помогают определить сущность

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