コンピュータサイエンスにおいて、タグ付き共用体(バリアント、バリアントレコード、選択型、判別共用体、非連結共用体、和型、またはコプロダクトとも呼ばれる)は、複数の異なる固定型を取り得る値を保持するために使用されるデータ構造です。一度に使用できる型は1つだけであり、タグフィールドによってどの型が使用されているかが明示的に示されます。これは、複数の「ケース」を持つ型と考えることができ、その型を操作する際には、それぞれのケースを正しく処理する必要があります。これは、再帰的なデータ型を定義する際に非常に重要です。再帰的なデータ型では、値の一部の要素がその値と同じ型を持つ場合があります。たとえば、マルチノードのサブツリーとリーフを区別する必要があるツリーを表す型を定義する場合などです。通常の共用体と同様に、タグ付き共用体では、一度に1つの型しか使用されないため、各型のストレージ領域を重複させることでストレージを節約できます。
タグ付き共用体は、 MLやHaskellなどの関数型プログラミング言語で最も重要であり、これらの言語ではデータ型(代数的データ型を参照)と呼ばれ、コンパイラはタグ付き共用体のすべてのケースが常に処理されることを検証できるため、多くの種類のエラーを回避できます。コンパイル時チェック付き和型はRustでも広く使用されており、列挙型と呼ばれています。ただし、タグ付き共用体はほぼすべてのプログラミング言語で構築でき、タグなし共用体(単に共用体と呼ばれることが多い)よりもはるかに安全です。タグなし共用体は似ていますが、共用体のどのメンバーが現在使用されているかを明示的に追跡しません。
タグ付き共用体には、コンストラクタという概念が伴うことがよくあります。これは、クラスのコンストラクタと似ていますが、同じではありません。コンストラクタとは、タグと対応する型の値を受け取ると、タグ付き共用体型の値を生成する関数または式のことです。
数学的には、タグ付き和集合は、通常 + を使用して表記される、非連結和集合または判別和集合に対応します。非連結和集合の要素が与えられた場合どこから来たのかを判断できるまたは要素が両方に存在する場合、その値の実質的に異なるコピーが 2 つ存在することになります。1つはそして1つは。
型理論では、タグ付き共用体は和型と呼ばれます。和型は積型の双対です。表記は様々ですが、通常は和型2種類の導入フォーム(注射剤)が付属していますそして排除形式はケース分析であり、MLスタイルの言語ではパターンマッチングとして知られています。型を持つそしてそして型を持つ以下の仮定の下でそしてそれぞれ、その用語型を持つ和型は、カリー・ハワード対応の下で直観主義論理的選言に対応する。
列挙型は、単位型のタグ付き共用体という特殊なケースと見なすことができます。これは、引数なしコンストラクタのセットに対応し、タグの値以外に追加のデータを保持しないため、単純なタグ変数として実装できます。
ロープ、遅延評価、クラス階層(下記参照)、任意精度演算、CDR コーディング、間接参照ビット、その他の種類のタグ付きポインタなど、多くのプログラミング技術やデータ構造は、通常、何らかのタグ付き共用体を使用して実装されます。
タグ付きユニオンは、最も単純な自己記述型データ形式と見なすことができます。タグ付きユニオンのタグは、最も単純なメタデータと見なすことができます。
フローセンシティブ型付けを持つ言語では、タグ付き共用体は共用体型とレコード型の組み合わせによって実装できます。[ 1 ]
タグ付き共用体のタグなし共用体に対する主な利点は、すべてのアクセスが安全であり、コンパイラがすべてのケースが適切に処理されていることを確認できる点です。タグなし共用体は、現在アクティブなフィールドを正しく識別するためにプログラムロジックに依存しており、そのロジックが失敗すると、予期しない動作や見つけにくいバグが発生する可能性があります。
各型ごとにフィールドを持つ単純なレコードと比較した場合、タグ付き共用体の主な利点は、すべての型のストレージを重複させることでストレージを節約できる点です。実装によっては、最大の型に十分なストレージを確保するものもあれば、必要に応じてタグ付き共用体の値のサイズを動的に調整するものもあります。値が不変である場合は、必要なだけのストレージを割り当てるのは簡単です。
タグ付き共用体の主な欠点は、タグがスペースを占有することです。通常、代替案は少数であるため、スペースが見つかる限り、タグは2ビットまたは3ビットに押し込められることが多いのですが、場合によってはこれらのビットさえも利用できないことがあります。このような場合、折りたたみタグ、計算タグ、またはエンコードタグが有効な代替手段となることがあります。これらのタグでは、タグの値は共用体フィールドの内容から動的に計算されます。一般的な例としては、予約値の使用が挙げられます。たとえば、正の数を返す関数が失敗を示すために-1を返す場合や、番兵値の使用などです。番兵値は、タグ付きポインタで最もよく使用されます。
タグなし共用体は、型間のビットレベル変換、つまり型変換を行うために使用されることがあります。タグ付き共用体は、このような目的で使用されることを想定していません。通常、タグが変更されるたびに新しい値が割り当てられます。
多くのプログラミング言語は、ある程度、普遍データ型をサポートしています。普遍データ型とは、他のすべての型のすべての値を含む型であり、多くの場合、普遍型の値の実際の型をテストする方法が提供されています。これらは、バリアントと呼ばれることもあります。普遍データ型は、形式的な定義ではタグ付き共用体と似ていますが、典型的なタグ付き共用体には比較的少数のケースしか含まれておらず、これらのケースは、データ構造ノードや命令などの単一の一貫した概念を表現するさまざまな方法を形成します。また、タグ付き共用体を使用する際には、考えられるすべてのケースが処理されることが期待されます。普遍データ型の値は関連付けられておらず、それらすべてを処理する現実的な方法はありません。
オプション型や例外処理と同様に、タグ付き共用体は例外的な結果の発生を処理するために使用されることがあります。多くの場合、これらのタグは予約値として型に組み込まれ、その出現は一貫してチェックされません。これはプログラミングエラーのかなり一般的な原因です。このタグ付き共用体の使用は、次の関数を持つモナドとして形式化できます。
どこそして共用型のコンストラクタです。そして有効な結果タイプであり、エラー条件のタイプです。あるいは、同じモナドは次のように記述することもできます。さらに2つの機能が追加されました。そして:
整数の二分木を構築したいとしましょう。MLでは、次のようなデータ型を作成することでこれを実現します。
データ型tree = Leaf | Node of ( int * tree * tree )これは2つのケースを持つタグ付き共用体です。1つはリーフで、ツリーのパスを終了させるために使用され、命令型言語におけるヌル値とほぼ同じように機能します。もう1つのブランチにはノードがあり、ノードには整数と左右のサブツリーが含まれています。LeafとNodeはコンストラクタであり、次のような特定のツリーを実際に生成することができます。
ノード( 5 ,ノード( 1 , Leaf , Leaf ),ノード( 3 , Leaf ,ノード( 4 , Leaf , Leaf )))これはこの木に対応します。

これで、例えばツリー内のノード数をカウントするような、型安全な関数を簡単に記述できます。
fun countNodes ( Leaf ) = 0 | countNodes ( Node ( int , left , right )) = 1 + countNodes ( left ) + countNodes ( right )ALGOL 68では、タグ付き共用体は統合モードと呼ばれ、タグは暗黙的に指定され、caseどのフィールドにタグが付いているかを判断するために構造が使用されます。
modenode = union (real, int, compl, string);
unioncaseofの使用例node:
node n := "1234"; case n in ( real r): print(("real:", r)), ( int i): print(("int:", i)), ( compl c): print(("compl:", c)), (文字列s): print(("string:", s)) out print(("?:", n)) esacALGOL 68 では、共用体は自動的により大きな共用体に変換されます。たとえば、その構成要素すべてがprintの共用体パラメータで処理できる場合、上記のout の場合のように、共用体を print に渡すだけで済みます。
Adaでは、これらは「識別型」と呼ばれます。
type Shape_Kind is ( Square , Rectangle , Circle ); type Shape ( Kind : Shape_Kind ) is record Center_X : Integer ; Center_Y : Integer ; case Kind is when Square => Side : Integer ; when Rectangle => Width , Height : Integer ; when Circle => Radius : Integer ; end case ; end record ;--判別式の特定の値に存在が依存するメンバーにアクセスしようとした際に、判別式が期待される値と異なると、エラーが発生します。Cではタグなし共用型である しか提供されていませんがunion、C++ではクラス( C++17以降) が提供されています。ただし、 のようなコア言語機能ではなく、標準ライブラリのクラスです。std::variant<Ts...>union
import std ;using std :: string ; using std :: variant ;struct Cat { string name ; };struct Dog { string name ; };struct Bird { string name ; };ペット= variant < Cat , Dog , Bird >を使用する;ペットp1 =猫( "ウィスカーズ" );ペットp2 =犬( "レックス" );「オーバーロードパターン」は、可変引数テンプレート継承を使用して代数的パターンマッチングを実装する一般的なデザインパターンです。[ 2 ]
// ビジターテンプレートのヘルパー型< typename ... Ts > struct Overload : public Ts ... { using Ts :: operator ()...; }// 推論ガイドテンプレート< typename ... Ts > Overload ( Ts ...) -> Overload < Ts ... > ;次に、std::visit()関数を使用してそれぞれのケースを呼び出すことができ、各ケースはラムダ式によって処理されます。[ 3 ]
値= variant < int , double , string >を使用します。値v = 3.14 ;// "double: 3.14" と出力しますstd :: visit ( Overload { []( int i ) -> void { std :: println ( "int: {}" , i ); }, []( double d ) -> void { std :: println ( "double: {}" , d ); }, []( const string & s ) -> void { std :: println ( "string: {}" , s ); } }, v );さらに、std::visit()以下の型を返すようにすることもできます。
ペットp =鳥( "ポリー" );string name = std :: visit ( Overload { [ ] ( const Cat & c ) -> string { return c.name ; }, [ ] ( const Dog & d ) -> string { return d.name ; }, []( const Bird & b ) - > string { return b.name ; } } , p ) ;// "ペットの名前: Polly" と出力しますstd :: println ( "ペットの名前: {}" , name );C++26以降、visit()以下のメソッドとして使用できます。std::variant<Ts...>
ペットp =鳥( "ポリー" );オーバーロードvisitor ( []( const Cat & c ) -> string { return c . name ; }, []( const Dog & d ) -> string { return d . name ; }, []( const Bird & b ) -> string { return b . name ; } );// "ペット名: Polly" と出力しますstd :: println ( "ペット名: { }" , p.visit ( visitor ));エラー処理のための結果型(Rustと同様)は、C++23でとして導入されました。std::result::Result<T,E>std::expected<T,E>
C#では従来、タグ付き共用体は存在しませんでした。これに最も近い方法は、record型に対するパターンマッチングでした。C# 15 では、キーワードを使用してタグ付き共用体が導入されましたunion。[ 4 ]
record class Car ( string Model ); record class Bicycle ( string Model ); record class Bus ( string Model );組合車両(乗用車、自転車、バス)Vehicle car = new Car ( " Tesla Model 3" ) ; Console.WriteLine ( car.Value ) ; // Car { Model = Tesla Model 3 }Vehicle bike = new Bicycle ( "Giant Escape 3" ); Console . WriteLine ( bike . Value ); // Bicycle { Model = Giant Escape 3 }Vehicle bus = new Bus ( "Volvo 9700" ); Console . WriteLine ( bus . Value ); // Bus { Model = Volvo 9700 }Vehicle v = /* ここに何らかの車両 */ ; string model = v switch { Car c => c . Model , Bicycle bk => b . Model , Bus bs => bs . Model , };default共用体の値は ですがnull、共用体内のすべての型がnull を許容しない場合は、switch式で をチェックする必要はありませんnull。
安全性が強化された C の方言であるCyclone は、タグ付きユニオンを提供します。[ 5 ]これらは@tagged修飾子付きで宣言されました。
@tagged union Foo { int i ; double d ; char * @fat s ; };void printFoo ( union Foo x ) { // 見落としたケースはコンパイラによって警告されますswitch ( x ) { case { . i = i }: printf ( "%d" , i ); break ; case { . d = d }: printf ( "%g" , d ); break ; case { . s = s }: printf ( "%s" , s ); break ; } }D は、 (任意の型を表す) や(代数的データ型を表す)std.variantなどの型を持つモジュールを提供します。 [ 6 ]Variantstd.variant.Algebraic!(T...)
import std.variant ;代数的!( int , string ) v = 10 ;int result = v.visit !( ( string s ) => cast ( int ) s.length , ( int i ) => i , () = > -1 ) ( ) ; writeln ( result ) ; // 10F#は労働組合を差別してきた。
type Tree = | Leaf |値のノード: int * left : Tree * right : Treelet tree = Node ( 5 , Node ( 1 , Leaf , Leaf ), Node ( 3 , Leaf , Node ( 4 , Leaf , Leaf )))定義されたケースは網羅的であるため、コンパイラはパターンマッチングにおいてすべてのケースが処理されることを確認できます。
ツリーを| Node ( x , _, _ ) -> printfn "トップレベルノードの値: %i" x | Leaf -> printfn "トップレベルノードはリーフです"Haxeの列挙型はタグ付き共用体としても機能します: [ 7 ]
enum Color { Red ; Green ; Blue ; Rgb ( r : Int , g : Int , b : Int ); }これらはスイッチ式を使用して照合できます。
switch ( color ) { case Red : trace ( "色は赤でした" ); case Green : trace ( "色は緑でした" ); case Blue : trace ( "色は青でした" ); case Rgb ( r , g , b ): trace ( "色の赤の値は " + rでした" ); }Javaでは、タグ付き共用体を実装する最も近い方法は、sealedクラスを使用することです。sealedクラスは、クラスを継承できる型を直接的に制限します。パターンマッチング自体は、switch式に対して実行できます。[ 8 ]
sealed interface Shape permits Circle , Rectangle , Triangle {} record Circle ( double radius ) implements Shape {} record Rectangle ( double width , double height ) implements Shape {} record Triangle ( double base , double height ) implements Shape {}double area ( Shape s ) { return switch ( shape ) { case Circle ( double r ) -> Math . PI * r * r ; case Rectangle ( double w , double h ) -> w * h ; case Triangle ( double b , double h ) -> 0.5 * b * h ; }; }Standard MLでは、タグ付き共用体は「代数的データ型」または「和型」です。
データ型shape =実数の円|実数の長方形*実数|点楽しい領域s =円rの場合=> 3.1415926535 * r * r |長方形( w , h ) => w * h |点= > 0.0Nimには、PascalやAdaのものと宣言が似ているオブジェクトバリアント[ 9 ]があります。
type ShapeKind = enum skSquare , skRectangle , skCircle Shape = object centerX , centerY : int case kind : ShapeKind of skSquare : side : int of skRectangle : length , height : int of skCircle : radius : intマクロは、パターンマッチングをエミュレートしたり、オブジェクトバリアントを宣言するための構文糖衣を作成したりするために使用できます。ここでは、patty パッケージによって実装されている例を示します。
パティをインポートproc `~` [ A ] ( a : A ): ref A = new ( result ) result [] = avariant List [ A ] : Nil Cons ( x : A , xs : ref List [ A ] )proc listHelper [ A ] ( xs : seq [ A ] ): List [ A ] = if xs . len == 0 : Nil [ A ] () else : Cons ( xs [ 0 ] , ~ listHelper ( xs [ 1 .. xs . high ] ))proc list [ A ] ( xs : varargs [ A ] ): List [ A ] = listHelper ( @ xs )proc sum ( xs : List [ int ] ): int = ( block : match xs : Nil : 0 Cons ( y , ys ): y + sum ( ys [] ) )echo sum ( list ( 1 , 2 , 3 , 4 , 5 ))OCamlでは、タグ付き共用体の構文は標準MLとは若干異なりますが、おおむね同じです。
型の形状= | floatの円| floatの長方形* float |ポイントlet area = function | Circle r -> Float . pi *. r *. r | Rectangle ( w , h ) -> w *. h | Point -> 0 . 0Pascalでは、これらは「バリアントレコード」と呼ばれます。
type shapeKind = ( square , rectangle , circle ) ; shape = record centerx : integer ; centery : integer ; case kind : shapeKind of square : ( side : integer ) ; rectangle : ( width , height : integer ) ; circle : ( radius : integer ) ; end ;Python 3.9 では、タグ付き共用型を定義するために使用できる型注釈のサポートが導入されました (PEP-593 [ 10 ] ):
from typing import Annotated , TypedDictCurrency = Annotated [ TypedDict ( 'Currency' , { 'dollars' : float , 'pounds' : float }, total = False ), TaggedUnion , ]Rust言語は、列挙型と呼ばれるタグ付き共用体を幅広くサポートしています。[ 11 ]例えば、
enum Tree { Leaf , Node ( i64 , Box < Tree > , Box < Tree > ) }また、共用体によるマッチングも可能です。
let tree : Tree = Tree :: Node ( 2 , Box :: new ( Tree :: Node ( 0 , Box :: new ( Tree :: Leaf ), Box :: new ( Tree :: Leaf ))), Box :: new ( Tree :: Node ( 3 , Box :: new ( Tree :: Leaf ), Box :: new ( Tree :: Node ( 4 , Box :: new ( Tree :: Leaf ), Box :: new ( Tree :: Leaf ))))) );fn add_values ( tree : Tree ) -> i64 { match tree { Tree :: Node ( v , a , b ) => v + add_values ( * a ) + add_values ( * b ), Tree :: Leaf => 0 } }fn main () { assert_eq! ( add_values ( tree ), 9 ); }Rust のエラー処理モデルは、これらのタグ付き共用体、特にまたは のいずれかである型と、または のいずれかである型に大きく依存しています。[ 12 ]std::option::Option<T>NoneSome(T)std::result::Result<T,E>Ok(T)Err(E)
Scalaにはケースクラスがあります。
sealed abstract class Tree case object Leaf extends Tree case class Node ( value : Int , left : Tree , right : Tree ) extends Treeval tree = Node ( 5 , Node ( 1 , Leaf , Leaf ), Node ( 3 , Leaf , Node ( 4 , Leaf , Leaf )))クラス階層がシールされているため、コンパイラはパターンマッチングですべてのケースが処理されることを確認できます。
tree match { case Node ( x , _ , _ ) => println ( "トップレベルノードの値: " + x ) case Leaf => println ( "トップレベルノードは葉ノードです" ) }Scalaのケースクラスは、サブタイピングによる再利用も可能にする。
sealed abstract class Shape ( centerX : Int , centerY : Int ) case class Square ( side : Int , centerX : Int , centerY : Int ) extends Shape ( centerX , centerY ) case class Rectangle ( length : Int , height : Int , centerX : Int , centerY : Int ) extends Shape ( centerX , centerY ) case class Circle ( radius : Int , centerX : Int , centerY : Int ) extends Shape ( centerX , centerY )Scala 3では列挙型が追加され、[ 13 ]以前のScalaの例をより簡潔に書き直すことができるようになりました。
enum Tree [ + T ]: case Leaf case Node ( x : Int , left : Tree [ T ], right : Tree [ T ])enum Shape ( centerX : Int , centerY : Int ): case Square ( side : Int , centerX : Int , centerY : Int ) extends Shape ( centerY , centerX ) case Rectangle ( length : Int , height : Int , centerX : Int , centerY : Int ) extends Shape ( centerX , centerY ) case Circle ( radius : Int , centerX : Int , centerY : Int ) extends Shape ( centerX , centerY )Swiftは列挙型によるタグ付き共用体も十分にサポートしています。[ 14 ]例えば、
enum Tree { case leaf indirect case node ( Int , Tree , Tree ) }let tree = Tree.node ( 2 , .node ( 0 , .leaf , .leaf ) , .node ( 3 , .leaf , .node ( 4 , .leaf , .leaf ) ) )func add_values ( _ tree : Tree ) -> Int { switch tree { case let . node ( v , a , b ): return v + add_values ( a ) + add_values ( b )case . leaf : return 0 } }assert ( add_values ( tree ) == 9 )TypeScriptでは、タグ付き共用体を作成することも可能です。例えば、次のようになります。
interface Leaf { kind : "leaf" ; }interface Node { kind : "node" ; value : number ; left : Tree ; right : Tree ; }型Tree = Leaf | Node ;const root : Tree = { kind : "node" , value : 5 , left : { kind : "node" , value : 1 , left : { kind : "leaf" }, right : { kind : "leaf" } }, right : { kind : "node" , value : 3 , left : { kind : "leaf" }, right : { kind : "node" , value : 4 , left : { kind : "leaf" }, right : { kind : "leaf" } } } };function visit ( tree : Tree ) : void { switch ( tree . kind ) { case "leaf" : break ; case "node" : console . log ( tree . value ); visit ( tree . left ); visit ( tree . right ); break ; } }オブジェクト指向プログラミングにおける典型的なクラス階層では、各サブクラスはそのクラス固有のデータをカプセル化できます。仮想メソッドの検索に使用されるメタデータ(例えば、ほとんどのC++実装におけるオブジェクトのvtableポインタ)はサブクラスを識別するため、インスタンスに格納されているデータを識別するタグとして機能します(RTTIを参照)。オブジェクトのコンストラクタはこのタグを設定し、オブジェクトの存続期間中、そのタグは一定に保たれます。
しかしながら、クラス階層には真のサブタイプ多相性が存在します。これは、同じ基本型のサブクラスをさらに作成することで拡張できますが、タグ/ディスパッチモデルでは正しく処理できません。そのため、タグ付き共用体の場合のように、サブオブジェクトの「タグ」に対してケース分析やディスパッチを行うことは通常できません。Scalaなどの一部の言語では、基本クラスを「シール」することができ、タグ付き共用体とシール付き基本クラスを統合できます。