Теговый союз
В информатике , тегированное объединение также называемое вариантом , вариантной записью , типом выбора , дискриминируемым объединением , непересекающимся объединением , типом суммы или копроизведением , представляет собой структуру данных, используемую для хранения значения, которое может принимать несколько разных, но фиксированных значений. типы. Одновременно может использоваться только один из типов, а поле тега явно указывает, какой тип используется. Его можно рассматривать как тип, имеющий несколько «случаев», каждый из которых должен корректно обрабатываться при манипулировании этим типом. Это имеет решающее значение при определении рекурсивных типов данных, в которых некоторый компонент значения может иметь тот же тип, что и это значение, например, при определении типа для представления деревьев , где необходимо различать многоузловые поддеревья и листья. Как и обычные объединения , тегированные объединения могут экономить место, перекрывая области хранения для каждого типа, поскольку одновременно используется только один из них.
Описание [ править ]
Теговые объединения наиболее важны в языках функционального программирования , таких как ML и Haskell , где они называются типами данных (см. Алгебраический тип данных ), и компилятор может проверить, что все случаи тегового объединения всегда обрабатываются, избегая многих типов ошибок. Типы проверяемых сумм во время компиляции также широко используются в Rust , где они называются enum . Однако их можно сконструировать практически на любом языке программирования , и они намного безопаснее, чем немаркированные объединения, часто называемые просто объединениями, которые похожи, но не отслеживают явно, какой член объединения используется в данный момент.
Теговые объединения часто сопровождаются концепцией конструктора , но не совпадает с конструктором класса который похож , . Конструктор — это функция или выражение, которое создает значение типа объединения тегов по заданному тегу и значению соответствующего типа.
Математически тегированные союзы соответствуют непересекающимся или дискриминируемым союзам , обычно записываемым с помощью +. По элементу непересекающегося союза A + B можно определить, произошел ли он A или B. из Если элемент находится в обоих, будут две фактически различные копии значения в A + B одна из A и одна из B. :
В теории типов тегированное объединение называется типом суммы . Типы сумм являются двойственными типами продуктов . Обозначения различаются, но обычно тип суммы A + B имеет две формы введения ( инъекции ) inj 1 : A → A + B и inj 2 : B → A + B . Форма исключения — это анализ случаев, известный как сопоставление с образцом в языках ML : если e имеет тип A + B , а e 1 и e 2 имеют тип в предположениях x : A и y : B соответственно, то член имеет тип . Тип суммы соответствует интуиционистской логической дизъюнкции при соответствии Карри-Ховарда .
Перечислимый тип можно рассматривать как вырожденный случай: тегированное объединение типов единиц . Он соответствует набору нулевых конструкторов и может быть реализован как простая переменная тега, поскольку он не содержит никаких дополнительных данных, кроме значения тега.
Многие методы программирования и структуры данных, включая веревку , отложенное вычисление , иерархию классов (см. ниже), арифметику произвольной точности , кодирование CDR , бит косвенности и другие виды тегированных указателей , обычно реализуются с использованием некоторого рода тегового объединения.
Объединение тегов можно рассматривать как простейший вид с самоописанием формата данных . Тег тегированного объединения можно рассматривать как простейший вид метаданных .
Преимущества и недостатки [ править ]
Основное преимущество тегированного объединения перед нетегированным объединением заключается в том, что любой доступ безопасен, и компилятор может даже проверить, что все случаи обработаны. Объединения без тегов зависят от логики программы, позволяющей правильно идентифицировать активное в данный момент поле, что может привести к странному поведению и труднообнаружимым ошибкам, если эта логика не сработает.
Основное преимущество объединения с тегами перед простой записью , содержащей поле для каждого типа, заключается в том, что оно экономит пространство за счет перекрытия хранилища для всех типов. Некоторые реализации резервируют достаточно места для самого большого типа, в то время как другие динамически корректируют размер значения тегированного объединения по мере необходимости. Когда значение неизменяемо , можно легко выделить столько места, сколько необходимо.
Основным недостатком объединений с тегами является то, что тег занимает место. Поскольку обычно существует небольшое количество альтернатив, тег часто можно сжать в 2 или 3 бита везде, где есть место, но иногда даже эти биты недоступны. В этом случае полезной альтернативой могут быть свернутые , вычисляемые или закодированные теги , где значение тега динамически вычисляется из содержимого поля объединения. Распространенными примерами являются использование зарезервированных значений , где, например, функция, возвращающая положительное число, может возвращать -1, указывая на сбой, и дозорные значения , чаще всего используемые в помеченных указателях .
Иногда нетегированные объединения используются для выполнения преобразований между типами на уровне битов, что в C++ называется переинтерпретацией приведения. Теговые союзы не предназначены для этой цели; обычно новое значение присваивается при каждом изменении тега.
Многие языки в некоторой степени поддерживают универсальный тип данных , который включает в себя все значения всех других типов, и часто предоставляется способ проверить фактический тип значения универсального типа. Их иногда называют вариантами . Хотя универсальные типы данных по формальному определению сравнимы с тегированными объединениями, типичные теговые объединения включают относительно небольшое количество случаев, и эти случаи образуют разные способы выражения одной связной концепции, такой как узел структуры данных или инструкция. Кроме того, ожидается, что каждый возможный случай объединения тегов будет рассмотрен при его использовании. Значения универсального типа данных не связаны друг с другом, и не существует реального способа справиться со всеми ними.
Подобно типам опций и обработке исключений , теговые объединения иногда используются для обработки исключительных результатов. Часто эти теги сворачиваются в тип как зарезервированные значения , и их появление не проверяется последовательно: это довольно распространенный источник ошибок программирования. Такое использование тегированных объединений можно формализовать как монаду со следующими функциями:
где «value» и «err» — конструкторы типа объединения, A и B — допустимые типы результатов, а E — тип условий ошибки. Альтернативно, та же монада может быть описана с помощью return и двух дополнительных функций, fmap и join :
Примеры [ править ]
Допустим, мы хотим построить двоичное дерево целых чисел. В ML мы бы сделали это, создав такой тип данных:
типов данных дерево = Лист
| Узел ( int * дерево * дерево )
Это объединение с тегами в двух случаях: один, лист, используется для завершения пути дерева и действует так же, как нулевое значение в императивных языках. Другая ветвь содержит узел, содержащий целое число, а также левое и правое поддерево. Leaf и Node — это конструкторы, которые позволяют нам создавать конкретное дерево, например:
Узел ( 5 , Узел ( 1 , Лист , Лист ), Узел ( 3 , Лист , Узел ( 4 , Лист , Лист )))
что соответствует этому дереву:
Теперь мы можем легко написать типобезопасную функцию, которая, например, подсчитывает количество узлов в дереве:
весело countNodes ( Лист ) = 0
| countNodes ( Node ( int , left , right )) =
1 + countNodes ( слева ) + countNodes ( справа )
График языковой поддержки [ править ]
1960-е годы [ править ]
В АЛГОЛе 68 теговые объединения называются объединенными модами , тег является неявным, а case
Конструкция используется для определения того, какое поле помечено:
mode node = union (real, int, compl, string);
Пример использования для union
case
из node
:
узел n:= "1234"; случай n в ( real r): print(("real:", r)), ( int i): print(("int:", i)), ( compl c): print(("compl:", c)), ( строка s): print(("строка:", s)) out print(("?:", n)) Эсак
1970-е и 1980-е годы [ править ]
Языки функционального программирования , такие как ML (с 1970-х годов) и Haskell (с 1990-х годов), отводят центральную роль теговым объединениям и имеют возможность проверять обработку всех случаев. Некоторые другие языки также поддерживают теговые объединения.
Pascal , Ada и Modula-2 называют их вариантными записями (формально различаемый тип в Ada) и требуют, чтобы поле тега было создано вручную и указаны значения тега, как в этом примере Pascal:
введите shapeKind = ( квадрат , прямоугольник , круг ) ;
форма = запись
centerx : целое число ;
центр : целое число ;
случая Вид : формаВид квадрата
( : число сторона : целое ) ;
прямоугольник : ( ширина , высота : целое число ) ;
круг : ( радиус : целое число ) ;
конец ;
и этот эквивалент Ada:
тип Shape_Kind ( Квадрат Прямоугольник , , ) Круг ;
тип Shape ( Kind : Shape_Kind ) — запись
Center_X : Integer ;
Center_Y : Целое число ;
Case Kind — это
когда Square =>
Side : Integer ;
когда Прямоугольник =>
Ширина , Высота : Целое число ;
когда Круг =>
Радиус : Целое число ;
конец дела ;
завершить запись ;
-- Любая попытка получить доступ к члену, существование которого зависит
-- от определенного значения дискриминанта, в то время как
-- дискриминант не является ожидаемым, вызывает ошибку.
В C и C++ тегированный союз может быть создан из нетегированного объединения с использованием строгой дисциплины доступа, при которой тег всегда проверяется:
enum ShapeKind { Квадрат , Прямоугольник , Круг };
структура Shape {
int centerx ;
интервал по центру ;
перечисление ShapeKind kind ;
объединение {
структура { int сторона ; }; /* Square */
struct { int width , height ; }; /* Прямоугольник */
struct { int radius ; }; /* Круг */
};
};
int getSquareSide ( struct Shape * s ) {
assert ( s -> kind == Square );
вернуть s -> сторона ;
}
void setSquareSide ( struct Shape * s , intside ) ; {
s > kind = Square -
s -> сторона = сторона ;
}
/* и так далее */
Пока доступ к полям объединения осуществляется только через функции, доступ будет безопасным и правильным. Тот же подход можно использовать для закодированных тегов; мы просто декодируем тег и затем проверяем его при каждом доступе. Если неэффективность этих проверок тегов вызывает беспокойство, они могут быть автоматически удалены в окончательной версии.
В C и C++ также имеется языковая поддержка одного конкретного объединения тегов: возможно нулевого указателя . Это можно сравнить с option
введите ML или Maybe
введите в Haskell и его можно рассматривать как тегированный указатель : тегированное объединение (с закодированным тегом) двух типов:
- Действительные указатели,
- Тип нулевого указателя только с одним значением,
null
, что указывает на исключительное состояние.
К сожалению, компиляторы C не проверяют, всегда ли обрабатывается нулевой случай. Это особенно распространенный источник ошибок в коде C, поскольку существует тенденция игнорировать исключительные случаи.
2000-е [ править ]
Один из продвинутых диалектов C, называемый Cyclone , имеет обширную встроенную поддержку теговых объединений. [1]
Типы перечислений в языках Rust , Haxe и Swift также работают как тегированные объединения.
Библиотека вариантов из библиотек Boost C++ продемонстрировала возможность реализации безопасного объединения тегов в виде библиотеки на C++, доступной для посещения с использованием объектов-функций.
struct display : boost :: static_visitor < void >
{
void оператор ()( int i )
{
std :: cout << "Это int со значением " << i << std :: endl ;
}
void оператор ()( const std :: string & s )
{
std :: cout << "Это строка со значением " << s << std :: endl ;
}
};
boost :: variant < int , std :: string > v = 42 ;
boost :: apply_visitor ( display (), v );
boost :: variant < int , std :: string > v = "привет, мир" ;
boost :: apply_visitor ( display (), v );
В Scala есть классы случаев:
запечатанный абстрактный класс Tree
Case Объект Leaf расширяет Tree
Case класс Node ( значение : Int , слева : Tree , справа : Tree ) расширяет Tree
val Tree = Node ( 5 , Node ( 1 , Leaf , Leaf ), Node ( 3 , Leaf , Node ) ( 4 , Лист , Лист )))
Поскольку иерархия классов изолирована, компилятор может проверить, что все случаи обрабатываются в соответствии с шаблоном:
дерева сопоставление {
case Node ( x , _ , _ ) => println ( «значение узла верхнего уровня: « + x )
case Leaf => println ( «узел верхнего уровня является листом» )
}
Классы Scala также допускают повторное использование посредством подтипирования:
запечатанный абстрактный класс Shape ( centerX : Int , centerY : Int )
случая класс Square ( сторона : Int , centerX : Int , centerY : Int ) расширяет форму ( centerX , centerY )
корпуса класс Rectangle ( длина : Int , высота : Int , centerX : Int , centerY : Int ) расширяет форму ( centerX , centerY )
корпуса класс Circle ( радиус : Int , centerX : Int , centerY : Int ) расширяет форму ( centerX , centerY )
В F# есть дискриминационные союзы:
тип Дерево =
| Лист
| Узел значения Leaf : int * left : Tree * right : Tree
let Tree = Node ( 5 , Node ( 1 , Leaf , ( , Node ( 3 , Leaf , Node 4 ) , Leaf , Leaf ))
Поскольку определенные случаи являются исчерпывающими, компилятор может проверить, что все случаи обрабатываются в соответствии с шаблоном:
совпадений дерево с
| Узел ( x , _, _) -> printfn «значение узла верхнего уровня: %i» x
| Leaf -> printfn «узел верхнего уровня является листом»
: Перечисления Haxe также работают как тегированные объединения [2]
перечисление Цвет {
Красный ;
Зеленый ;
Синий ;
Rgb ( r : Int , g : Int , b : Int );
}
Их можно сопоставить с помощью выражения переключателя:
переключатель ( цвет ) {
case Red : трассировка ( «Цвет был красный» );
case Green : трассировка ( «Цвет был зеленый» );
case Blue : трассировка ( «Цвет был синий» );
case Rgb ( r , g , b ): трассировка ( «Цвет имел красное значение « + r );
}
У Нима есть варианты объектов [3] объявление аналогично объявлениям в Паскале и Аде:
тип
ShapeKind = enum
skSquare , skRectangle , skCircle
Shape = объект
centerX , centerY : int
случая тип : ShapeKind
из skSquare :
сторона : int
из skRectangle :
длина , высота : int
из skCircle :
радиус : int
Макросы можно использовать для эмуляции сопоставления с образцом или для создания синтаксического сахара для объявления вариантов объекта, как показано здесь, как это реализовано пакетом patty :
import patty
proc `~` [ A ] ( a : A ): A = новый
( result ) result
[ ] = вариант
A List [ A ] :
Nil
Cons ( x : , ref xs : ref List [ A ] )
proc listHelper [ A ] ( xs : seq [ A ] ): Список [ A ] =
if xs . len == 0 : Nil [ A ] ()
else : Cons ( xs [ 0 ] , ~ listHelper ( xs [ 1 .. xs . high ] ))
процедур список [ A ] ( xs : varargs [ A ] ): List [ A ] = listHelper ( @ xs )
proc sum ( xs : List [ int ] ): int = ( блок :
match xs :
Nil : 0
Cons ( y , ys ): y + sum ( ys [] )
)
echo sum ( список ( 1 , 2 , 3 , 4 , 5 ))
2010-е [ править ]
Перечисления добавлены в Scala 3, [4] что позволяет нам более кратко переписать предыдущие примеры Scala:
enum Tree [ + T ]:
регистр Leaf
Case Node ( x : Int , слева : Tree [ T ], справа : Tree [ T ])
enum Shape ( centerX : Int , centerY : Int ):
case Square ( сторона : Int , centerX : Int , centerY : Int ) расширяет Shape ( centerY , centerX )
регистр Прямоугольник ( длина : Int , высота : Int , centerX : Int , centerY : Int ) расширяет Shape ( centerX , centerY )
регистр Круг ( радиус : Int , centerX : Int) , centerY : Int ) расширяет форму ( centerX , centerY )
В языке Rust имеется обширная поддержка объединений тегов, называемых перечислениями. [5] Например:
enum Tree {
Лист ,
Узел ( i64 , Box < Дерево > , Box < Дерево > )
}
Он также позволяет сопоставлять союзы:
let Tree = Дерево :: Узел (
2 ,
Коробка :: новый ( Дерево :: Узел ( 0 , Коробка :: новый ( Дерево :: Лист ), Коробка :: новый ( Дерево :: Лист ))),
Коробка :: новый ( Дерево :: Узел ( 3 , Коробка :: новый ( Дерево :: Лист ),
Коробка :: новый ( Дерево :: Узел ( 4 , Коробка :: новый ( Дерево :: Лист ), Коробка :: новый ( Дерево :: Лист )))))
);
fn add_values ( tree : Tree ) -> i64 {
match Tree {
Tree :: Node ( v , a , b ) => v + add_values ( * a ) + add_values ( * b ),
Tree :: Leaf => 0
}
}
утверждать_eq! ( add_values ( дерево ), 9 );
Модель обработки ошибок в Rust во многом опирается на эти теговые объединения, особенно на Option<T>
тип, который либо None
или Some(T)
и Result<T, E>
тип, который либо Ok(T)
или Err(E)
. [6]
Swift также имеет существенную поддержку тегированных объединений посредством перечислений. [7] Например:
enum Tree {
случая лист
косвенный случая узел ( Int , Tree , Tree )
}
let Tree = Tree . узел (
2 ,
.узел 0 ( , , .лист .лист , 4 , )
,. узел ( 3 , .лист Tree .узел ) .лист ( .лист , Int переключатель )
)
func add_values ( _tree : > - {
дерево {
случай пусть . узел ( v , a , b ):
return v + add_values ( a ) + add_values ( b )
case . лист :
вернуть 0
}
}
утверждать ( add_values ( дерево ) == 9 )
С помощью TypeScript также можно создавать теговые объединения. Например:
интерфейс Leaf { kind : "leaf" ; }
интерфейса Узел { вид : "узел" ; значение : число ; слева : Дерево ; справа : Дерево ; }
Тип Дерево = Лист | узла
Константный корень : Дерево = {
вид : "узел" ,
значение : 5 ,
влево : {
вид : "узел" ,
значение : 1 ,
влево : { вид : "лист" },
вправо : { вид : "лист" }
} ,
вправо : {
вид : "узел" ,
значение : 3 ,
влево : { вид : "лист" },
вправо : {
вид : "узел" ,
значение : 4 ,
влево : { вид : "лист" },
вправо : { вид : "лист" }
}
}
}
функция посещения ( дерево : Дерево ) {
переключатель ( дерево . вид ) {
случай "лист" :
разрыв
случая "узел" :
консоль . журнал ( дерево . значение )
посещение ( дерево . влево )
посещение ( дерево . вправо )
перерыв
}
}
В Python 3.9 появилась поддержка ввода аннотаций, которые можно использовать для определения типа объединения тегов (PEP-593). [8] ):
Валюта = Аннотация [
TypedDict ( 'Currency' , { 'доллары' : float , 'фунты' : float }, total = False ),
TaggedUnion ,
]
В C++17 представлены std::variant и constexpr, если
используя Tree = std :: variant < struct Leaf , struct Node > ;
struct Leaf
{
std :: строковое значение ;
};
struct Node
{
Дерево * left = nullptr ;
Дерево * вправо = nullptr ;
};
struct Transverser
{
шаблон < имя типа T >
void оператор ()( T && v )
{
if constexpr ( std :: is_same_v < T , Leaf &> )
{
std :: cout << v . значение << " \n " ;
}
else if constexpr ( std :: is_same_v < T , Node &> )
{
if ( v . left != nullptr )
std :: visit ( Transverser {}, * v . left );
if ( v . right != nullptr )
std :: visit ( Transverser {}, * v . right );
}
else
{
// Выражение !sizeof(T) всегда ложно
static_assert ( ! sizeof ( T ), "неполный посетитель!" );
};
}
};
/*Деревянный лес = ...;
std::visit(Transverser{}, Forest);*/
Иерархии классов как тегированные объединения [ править ]
типичной иерархии классов В объектно-ориентированного программирования каждый подкласс может инкапсулировать данные, уникальные для этого класса. Метаданные, используемые для поиска виртуальных методов объекта (например, указатель vtable в большинстве реализаций C++), идентифицируют подкласс и поэтому эффективно действуют как тег, идентифицирующий данные, хранящиеся в экземпляре (см. RTTI ). объекта Конструктор устанавливает этот тег, и он остается постоянным на протяжении всего существования объекта.
Тем не менее, иерархия классов предполагает истинный полиморфизм подтипов . Его можно расширить путем создания дополнительных подклассов того же базового типа, которые невозможно правильно обработать в рамках модели тегов/отправки. Следовательно, обычно невозможно выполнить анализ вариантов или отправку «тега» подобъекта, как это было бы в случае с тегированными объединениями. Некоторые языки, такие как Scala, позволяют «запечатывать» базовые классы и объединять тегированные объединения с запечатанными базовыми классами.
См. также [ править ]
- Дискриминатор , тег типа для дискриминируемых объединений в CORBA.
- Тип варианта (COM)
Ссылки [ править ]
- ^ «Циклон: Союзы с тегами» .
- ^ «Использование перечислений — Haxe — кроссплатформенный набор инструментов» . Фонд Хаксе.
- ^ «Руководство Нима» . nim-lang.org . Проверено 23 января 2020 г.
- ^ «Справочник по языку Scala 3: перечисления» . Команда Скала.
- ^ «Язык программирования Rust» . Мозилла.
- ^ «Ржавчина на примере» . Мозилла.
- ^ «Перечисления — язык программирования Swift (Swift 5.4)» . docs.swift.org . Проверено 28 апреля 2021 г.
- ^ «PEP 593 — Гибкие функции и аннотации переменных» . Python.org . Проверено 20 июня 2021 г.
Внешние ссылки [ править ]
- boost::variant — это типобезопасное дискриминируемое объединение C++.
- std.variant — это реализация варианта типа в D 2.0.