Введение
Структура данных, используемая для хранения значения, которое может принимать несколько различных, но фиксированных типов. В информатике, маркированный союз, также называемый вариантом, вариантной записью, типом выбора, дискриминированным объединением, непересекающимся объединением, суммарным типом или копроизведением, — это структура данных, предназначенная для хранения значения, которое может быть одного из нескольких предопределённых типов. В любой момент времени используется только один из этих типов, и поле-тег явно указывает, какой именно тип активен. Его можно рассматривать как тип, имеющий несколько "вариантов", каждый из которых должен быть обработан корректно при работе с этим типом. Это критически важно при определении рекурсивных типов данных, в которых некоторые компоненты значения могут иметь тот же тип, что и само значение, например, при определении типа для представления деревьев, где необходимо различать поддеревья с несколькими узлами и листья. Как и обычные объединения, маркированные объединения могут экономить память, перекрывая области хранения для разных типов, поскольку одновременно используется только один из них.
In computer science, a tagged union, also called a variant, variant record, choice type, discriminated union, disjoint union, sum type, or coproduct, is a data structure used to hold a value that could take on several different, but fixed, types. Only one of the types can be in use at any one time, and a tag field explicitly indicates which type is in use. It can be thought of as a type that has several "cases", each of which should be handled correctly when that type is manipulated. This is critical in defining recursive datatypes, in which some component of a value may have the same type as that value, for example in defining a type for representing trees, where it is necessary to distinguish multi node subtrees and leaves. Like ordinary unions, tagged unions can save storage by overlapping storage areas for each type, since only one is in use at a time.
Описание
Тегированные объединения наиболее важны в функциональных языках программирования, таких как ML и Haskell, где они называются типами данных (см. алгебраический тип данных), и компилятор может проверить, что все случаи тегированного объединения всегда обрабатываются, избегая многих типов ошибок. Типы с проверкой суммы во время компиляции также широко используются в Rust, где они называются `enum`. Однако их можно построить практически на любом языке программирования, и они намного безопаснее нетегированных объединений, часто называемых просто объединениями, которые похожи, но не отслеживают, какой член объединения в настоящее время используется. Тегированные объединения часто сопровождаются концепцией конструктора, который похож, но не идентичен конструктору для класса. Конструктор — это функция или выражение, которое производит значение тегированного типа объединения, заданное тегом и значением соответствующего типа. Математически тегированные объединения соответствуют непересекающимся или дискриминированным объединениям, обычно записываемым с помощью +. Для элемента непересекающегося объединения A + B можно определить, из A он или из B. Если элемент принадлежит обоим, в A + B будет две фактически различные копии значения, одна из A и одна из B. В теории типов тегированное объединение называется типом суммы. Типы суммы являются двойственными типам произведений. Обозначения различаются, но обычно тип суммы A + B включает две формы введения (инъекции): inj1: A → A + B и … Форма устранения — это анализ случаев, известный как сопоставление с образцом в языках в стиле ML: если e имеет тип A + B, а e1 и e2 имеют тип при предположениях x: A и y: B соответственно, то термин имеет тип. Тип суммы соответствует интуиционистскому логическому дизъюнкции в соответствии с соответствием Карри — Ховарда. Перечисляемый тип можно рассматривать как вырожденный случай: тегированное объединение типов-единиц. Он соответствует набору конструкторов с нулевыми аргументами и может быть реализован как простая переменная тега, поскольку не содержит дополнительных данных, кроме значения тега. Многие методы программирования и структуры данных, включая rope, ленивые вычисления, иерархию классов (см. ниже), арифметику произвольной точности, кодирование CDR, бит косвенности и другие виды помеченных указателей, обычно реализуются с использованием того или иного тегированного объединения. Тегированное объединение можно рассматривать как простейший вид самоописываемого формата данных. Тег тегированного объединения можно рассматривать как простейший вид метаданных.
Преимущества и недостатки
Основное преимущество маркированного союза над немаркированным союзом заключается в том, что все обращения к данным безопасны, и компилятор может даже проверить, что все возможные случаи обработаны. Немаркированные союзы полагаются на логику программы для правильной идентификации текущего активного поля, что может привести к непредсказуемому поведению и трудноуловимым ошибкам, если эта логика окажется неверной. Основное преимущество маркированного союза перед простой записью, содержащей поле для каждого типа, заключается в экономии памяти за счет совместного использования памяти для всех типов. Некоторые реализации резервируют достаточно памяти для самого большого типа, в то время как другие динамически изменяют размер маркированного союза по мере необходимости. Когда значение неизменно, можно выделить ровно столько памяти, сколько требуется. Главный недостаток маркированных союзов – метка занимает место. Поскольку обычно число альтернатив невелико, метку часто можно уместить в 2 или 3 бита, где только возможно, но иногда даже этих битов недостаточно. В этом случае полезной альтернативой могут быть свернутые, вычисленные или закодированные метки, когда значение метки динамически вычисляется на основе содержимого поля союза. Распространенными примерами являются использование зарезервированных значений, когда, например, функция, возвращающая положительное число, может вернуть 1 для обозначения ошибки, и сигнальные значения, чаще всего используемые в меченных указателях. Иногда немаркированные союзы используются для выполнения преобразований на уровне битов между типами, называемых reinterpret_cast в C++. Маркированные союзы не предназначены для этой цели; обычно новое значение присваивается при изменении метки. Многие языки в той или иной степени поддерживают универсальный тип данных, который включает в себя все значения всех других типов, и часто предоставляется способ проверки фактического типа значения универсального типа. Их иногда называют вариантами. Хотя универсальные типы данных сопоставимы с маркированными союзами в их формальном определении, типичные маркированные союзы включают относительно небольшое количество случаев, и эти случаи представляют собой различные способы выражения единой согласованной концепции, такие как узел структуры данных или инструкция. Кроме того, предполагается, что каждый возможный случай маркированного союза будет обработан при его использовании. Значения универсального типа данных не связаны между собой, и нет практического способа обработать их все. Как и типы Option и обработка исключений, маркированные союзы иногда используются для обработки исключительных результатов. Часто эти метки включаются в тип в качестве зарезервированных значений, и их появление не всегда проверяется последовательно: это довольно распространенный источник ошибок программирования. Это использование маркированных союзов можно формализовать как монаду со следующими функциями: где "value" и "err" – конструкторы типа союза, A и B – допустимые типы результатов, а E – тип условий ошибки. В качестве альтернативы, та же монада может быть описана функцией return и двумя дополнительными функциями, fmap и join:
where "value" and "err" are the constructors of the union type, A and B are valid result types and E is the type of error conditions. Alternately, the same monad may be described by return and two additional functions, fmap and join:
Классовые иерархии как маркированные союзы
В типичной иерархии классов в объектно-ориентированном программировании каждый подкласс может инкапсулировать данные, уникальные для этого класса. Метаданные, используемые для поиска виртуальных методов (например, указатель vtable объекта в большинстве реализаций C++), идентифицируют подкласс и, таким образом, фактически служат тегом, идентифицирующим данные, хранящиеся экземпляром (см. RTTI). Конструктор объекта устанавливает этот тег, и он остаётся неизменным на протяжении всего времени жизни объекта. Тем не менее, иерархия классов обеспечивает истинный полиморфизм подтипов. Её можно расширить, создавая новые подклассы того же базового типа, что невозможно корректно обработать в модели тегов/диспетчеризации. Поэтому, как правило, невозможно выполнять анализ вариантов или диспетчеризацию по "тегу" подобъекта, как это делается для объединений с тегами. Некоторые языки, такие как Scala, позволяют "запечатывать" базовые классы, объединяя таким образом объединения с тегами и запечатанные базовые классы.