Taggad fackförening
Inom datavetenskap är en taggad union , även kallad variant , variantpost , valtyp , diskriminerad union , disjunkt union , summatyp eller samprodukt , en datastruktur som används för att hålla ett värde som kan anta flera olika, men fasta, typer . Endast en av typerna kan användas åt gången, och ett taggfält anger uttryckligen vilken som används. Det kan ses som en typ som har flera "cases", som vart och ett ska hanteras korrekt när den typen manipuleras. Detta är avgörande för att definiera rekursiva datatyper, där någon komponent av ett värde kan ha samma typ som själva värdet, till exempel för att definiera en typ för att representera träd, där det är nödvändigt att särskilja multinodsunderträd och löv. Liksom vanliga fackföreningar kan taggade fackföreningar spara lagring genom att överlappa lagringsutrymmen för varje typ, eftersom endast en används åt gången.
Beskrivning
Taggade fackföreningar är viktigast i funktionella språk som ML och Haskell , där de kallas datatyper (se algebraisk datatyp ) och kompilatorn kan verifiera att alla fall av ett taggat förbund alltid hanteras, vilket undviker många typer av fel. De kan dock konstrueras på nästan vilket språk som helst och är mycket säkrare än otaggade fackföreningar, ofta helt enkelt kallade fackföreningar, som liknar varandra men inte uttryckligen håller reda på vilken medlem av facket som för närvarande används.
Taggade fackföreningar åtföljs ofta av konceptet med en typkonstruktor , som är liknande men inte samma som en konstruktor för en klass. Typkonstruktörer producerar en taggad unionstyp, givet den ursprungliga taggtypen och motsvarande typ.
Matematiskt motsvarar taggade fackföreningar osammanhängande eller diskriminerade fackföreningar , vanligtvis skrivna med +. Givet ett element av en disjunkt förening A + B , är det möjligt att avgöra om det kom från A eller B. Om ett element ligger i båda kommer det att finnas två effektivt distinkta kopior av värdet i A + B , en från A och en från B .
I typteorin kallas en taggad union en summatyp . Summatyper är de dubbla produkttyperna . Notationerna varierar, men vanligtvis kommer summatypen A + B med två introduktionsformer ( injektioner ) inj 1 : A → A + B och inj 2 : B → A + B . Elimineringsformuläret är fallanalys, känd som mönstermatchning i programmeringsspråk av ML-stil : om e har typ A + B och e 1 och e 2 har typ under antagandena x : A och y : B resp har typen . Summatypen motsvarar intuitionistisk logisk disjunktion under Curry-Howard-korrespondensen .
En uppräknad typ kan ses som ett degenererat fall: en märkt förening av enhetstyper . Den motsvarar en uppsättning nullära konstruktörer och kan implementeras som en enkel taggvariabel, eftersom den inte innehåller några ytterligare data förutom taggens värde.
Många programmeringstekniker och datastrukturer, inklusive rep , lat utvärdering , klasshierarki (se nedan), aritmetik med godtycklig precision , CDR-kodning , inriktningsbiten och andra typer av taggade pekare , etc. implementeras vanligtvis med någon sorts taggad union.
Ett taggat förbund kan ses som den enklaste typen av självbeskrivande dataformat . Taggen för den taggade föreningen kan ses som den enklaste typen av metadata .
Fördelar och nackdelar
Den främsta fördelen med en taggad union framför en otaggad union är att alla åtkomster är säkra, och kompilatorn kan till och med kontrollera att alla ärenden hanteras. Otaggade fackföreningar är beroende av programlogik för att korrekt identifiera det för närvarande aktiva fältet, vilket kan resultera i konstigt beteende och svåra att hitta buggar om den logiken misslyckas.
Den primära fördelen med en taggad union jämfört med en enkel post som innehåller ett fält för varje typ är att den sparar lagring genom att överlappa lagring för alla typer. Vissa implementeringar reserverar tillräckligt med lagringsutrymme för den största typen, medan andra dynamiskt justerar storleken på ett taggat unionsvärde efter behov. När värdet är oföränderligt är det enkelt att allokera precis så mycket lagringsutrymme som behövs.
Den största nackdelen med taggade fackföreningar är att taggen tar plats. Eftersom det vanligtvis finns ett litet antal alternativ, kan taggen ofta klämmas in i 2 eller 3 bitar varhelst utrymme kan hittas, men ibland är inte ens dessa bitar tillgängliga. I det här fallet kan ett användbart alternativ vara vikta , beräknade eller kodade taggar , där taggvärdet beräknas dynamiskt från innehållet i unionsfältet. Vanliga exempel på detta är användningen av reserverade värden , där till exempel en funktion som returnerar ett positivt tal kan returnera -1 för att indikera fel, och sentinel-värden , som oftast används i taggade pekare .
Ibland används otaggade fackföreningar för att utföra konverteringar på bitnivå mellan typer, så kallade omtolkningar i C++. Taggade fackföreningar är inte avsedda för detta ändamål; vanligtvis tilldelas ett nytt värde när taggen ändras.
Många språk stödjer i viss utsträckning en universell datatyp , vilket är en typ som inkluderar alla värden av alla andra typer, och ofta tillhandahålls ett sätt att testa den faktiska typen av ett värde av den universella typen. Dessa kallas ibland för varianter . Medan universella datatyper är jämförbara med taggade fackföreningar i sin formella definition, inkluderar typiska taggade fackföreningar ett relativt litet antal fall, och dessa fall bildar olika sätt att uttrycka ett enda sammanhängande koncept, såsom en datastrukturnod eller instruktion. Det finns också en förväntning om att alla möjliga fall av ett taggat fackförbund kommer att behandlas när det används. Värdena för en universell datatyp är inte relaterade och det finns inget genomförbart sätt att hantera dem alla.
Liksom alternativtyper och undantagshantering används ibland taggade fackföreningar för att hantera förekomsten av exceptionella resultat. Ofta viks dessa taggar till typen som "reserverade värden", och deras förekomst kontrolleras inte konsekvent: detta är en ganska vanlig källa till programmeringsfel. Denna användning av taggade fackföreningar kan formaliseras som en monad med följande funktioner:
där "värde" och "fel" är konstruktörerna av unionstypen, A och B är giltiga resultattyper och E är typen av feltillstånd. Alternativt kan samma monad beskrivas med retur och två ytterligare funktioner, fmap och join :
Exempel
Säg att vi ville bygga ett binärt träd av heltal. I ML skulle vi göra detta genom att skapa en datatyp så här:
datatypträd = Blad | _ Nod av ( int * träd * träd )
Detta är en taggad union med två fall: ett, bladet, används för att avsluta en bana i trädet och fungerar ungefär som ett nollvärde skulle göra i imperativa språk. Den andra grenen har en nod, som innehåller ett heltal och ett vänster och höger underträd. Leaf och Node är konstruktörerna som gör det möjligt för oss att faktiskt producera ett visst träd, till exempel:
Nod ( 5 , Nod ( 1 , Leaf , Leaf ), Nod ( 3 , Leaf , Node ( 4 , Leaf , Leaf )))
som motsvarar detta träd:
Nu kan vi enkelt skriva en typsäker funktion som, säg, räknar antalet noder i trädet:
0
fun countNoder ( Leaf ) = | countNodes ( Node ( int , vänster , höger )) = 1 + countNodes ( vänster ) + countNodes ( höger )
Tidslinje för språkstöd
1960-talet
I ALGOL 68 kallas taggade fackföreningar för united modes , taggen är implicit och case
-konstruktionen används för att bestämma vilket fält som är taggat:
mode node = union ( real , int , compl , sträng );
Användningsexempel för unionsfall
av
nod :
nod n := "1234"; case n in ( real r): print(("real:", r)), ( int i): print(("int:", i)), ( compl c): print(("compl:", c)), ( sträng s): print(("sträng:", s)) out print(("?:", n)) esac
1970- och 1980-talen
Även om i första hand endast funktionella språk som ML (från 1970-talet) och Haskell (från 1990-talet) ger en central roll till taggade förbund och har befogenhet att kontrollera att alla ärenden hanteras, har andra språk stöd även för taggade förbund. Men i praktiken kan de vara mindre effektiva i icke-funktionella språk på grund av optimeringar som aktiveras av funktionella språkkompilatorer som kan eliminera explicita taggkontroller och undvika explicit lagring av taggar . [ citat behövs ]
Pascal , Ada och Modula-2 kallar dem variantposter (formellt diskriminerad typ i Ada) och kräver att taggfältet skapas manuellt och taggvärdena specificeras, som i detta Pascal-exempel:
typ shapeKind = ( kvadrat , rektangel , cirkel ) ; shape = post centerx : heltal ; centery : heltal ; case type : shapeKind of square : ( sida : heltal ) ; rektangel : ( bredd , höjd : heltal ) ; cirkel : ( radie : heltal ) ; slut ;
och denna Ada-motsvarighet:
typ Shape_Kind är ( Square , Rectangle , Circle ); typ Shape ( Kind : Shape_Kind ) är posten Center_X : Heltal ; Center_Y : Heltal ; case Typ är när kvadrat => sida : heltal ; när rektangel => bredd , höjd : heltal ; när Cirkel => Radie : Heltal ; slutfall ; _ slutpost ; -- Varje försök att komma åt en medlem vars existens beror -- på ett visst värde hos diskriminanten, medan -- diskriminanten inte är den förväntade, ger upphov till ett fel.
I C och C++ kan en taggad fackförening skapas från otaggade fackföreningar med en strikt åtkomstdisciplin där taggen alltid kontrolleras:
enum ShapeKind { Square , Rectangle , Circle }; struct Shape { int centerx ; int centery ; enum ShapeKind typ ; union { struct { int sida ; }; /* Square */ struct { int width , height ; }; /* Rektangel */ struct { int radius ; }; /* Cirkel */ }; }; int getSquareSide ( struct Shape * s ) { assert ( s -> kind == Square ); returnera s -> sida ; } void setSquareSide ( struct Shape * s , int sida ) { s -> kind = Square ; s -> sida = sida ; } /* och så vidare */
Så länge förbundsfälten endast nås via funktionerna kommer åtkomsterna att vara säkra och korrekta. Samma tillvägagångssätt kan användas för kodade taggar; vi avkodar helt enkelt taggen och kontrollerar den sedan vid varje åtkomst. Om ineffektiviteten hos dessa taggkontroller är ett problem kan de tas bort automatiskt i den slutliga versionen.
C och C++ har också språkstöd för ett särskilt taggat förbund: possibly-null- pekaren . Detta kan jämföras med alternativtypen
i ML eller Maybe
-typen i Haskell, och kan ses som en taggad pekare : en taggad union (med en kodad tagg) av två typer:
- Giltiga tips,
- En nollpekartyp med bara ett värde,
null
, som indikerar ett exceptionellt tillstånd.
Tyvärr verifierar inte C-kompilatorer att noll-fallet alltid hanteras, och detta är en särskilt utbredd källa till fel i C-kod, eftersom det finns en tendens att ignorera exceptionella fall.
2000-talet
En avancerad dialekt av C som kallas Cyclone har omfattande inbyggt stöd för taggade fackföreningar.
Enumtyperna i språken Rust , Haxe och Swift fungerar också som taggade fackföreningar.
Variantbiblioteket från Boost har visat att det var möjligt att implementera en säker taggad union som ett bibliotek i C++, som kan besökas med funktionsobjekt.
struct display : boost :: static_visitor < void > { void operator ()( int i ) { std :: cout << "Det är en int, med värde " << i << std :: endl ; } void operator ()( const std :: string & s ) { std :: cout << "Det är en sträng, med värde " << s << std :: endl ; } }; boost :: variant < int , std :: sträng > v = 42 ; boost :: applicera_besökare ( visa (), v ); boost :: variant < int , std :: string > v = "hej värld" ; boost :: applicera_besökare ( visa (), v );
Scala har fallklasser:
förseglad abstrakt klass Trädfallsobjekt Löv sträcker sig Trädfallsklass Nod ( värde : Int , vänster : Träd , höger : Träd ) sträcker sig Trädval träd = Nod ( 5 , Nod ( 1 , Löv , Löv ) , Nod ( 3 , Löv , Nod ) _ ( 4 , Leaf , Leaf )))
Eftersom klasshierarkin är förseglad kan kompilatorn kontrollera att alla ärenden hanteras i en mönstermatchning:
tree match { case Node ( x , _ , _ ) => println ( "top level node value: " + x ) case Leaf => println ( "top level node is a leaf" ) }
Scalas fallklasser tillåter också återanvändning genom subtyping:
förseglad abstrakt klass Form ( centerX : Int , centerY : Int ) fallklass Square ( sida : Int , centerX : Int , centerY : Int ) sträcker sig Form ( centerX , centerY ) fallklass Rektangel ( längd : Int , höjd : Int , centerX : _ Int , centerY : Int ) utökar Form ( centerX , centerY ) fallklass Cirkel ( radie : Int , centerX : Int , centerY : Int ) utökar Form ( centerX , centerY ) _
F# har diskriminerat fackföreningar:
typ Träd = | Löv | Värdenod : int * vänster : Träd * höger : Träd låt träd = Nod ( 5 , Nod ( 1 , Leaf , Leaf ) , Nod ( 3 , Leaf , Nod ( 4 , Leaf , Leaf )) )
Eftersom de definierade fallen är uttömmande kan kompilatorn kontrollera att alla fall hanteras i en mönstermatchning:
matcha träd med | Nod ( x , _, _) -> printfn "toppnivå nodvärde: %i" x | Leaf -> printfn "toppnivånod är ett löv"
Haxes enums fungerar också som taggade fackföreningar:
enum Färg { Röd ; Grönt ; Blå ; Rgb ( r : Int , g : Int , b : Int ); }
Dessa kan matchas med ett switchuttryck:
switch ( color ) { case Red : trace ( "Färgen var röd" ); case Green : trace ( "Färgen var grön" ); case Blue : trace ( "Färgen var blå" ); case Rgb ( r , g , b ): trace ( "Färgen hade ett rött värde på" + r ); }
Nim har objektvarianter som liknar deklarationen i Pascal och Ada:
typ ShapeKind = enum skSquare , skRektangel , skCirkel Form = objekt centerX , centerY : int case type : ShapeKind av skSquare : sida : int av skRektangel : längd , höjd : int av skCircle : radie : int
Makron kan användas för att emulera mönstermatchning eller för att skapa syntaktisk socker för att deklarera objektvarianter, ses här som implementerat av paketet patty :
0
0
0
import patty proc `~` [ A ] ( a : A ) : ref A = nytt ( resultat ) resultat [ ] = en variant Lista [ A ] : Noll Cons ( x : A , xs : ref List [ A ] ) proc listHjälp [ A ] ( xs : seq [ A ] ): Lista [ A ] = om xs . len == : Noll [ A ] () annars : Nackdelar ( xs [ ] , ~ listHelper ( xs [ 1 .. xs . high ] )) proc lista [ A ] ( xs : varargs [ A ] ): Lista [ A ] = listHelper ( @ xs ) proc summa ( xs : List [ int ] ): int = ( block : match xs : Noll : Nackdelar ( y , ys ) : y + summa ( ys [ ] ) ) ekosumma ( lista ( 1 , 2 , 3 , 4 , 5 ))
2010-talet
Enums läggs till i Scala 3, vilket gör att vi kan skriva om de tidigare Scala-exemplen mer kortfattat:
enum Träd [ + T ] : kasus Lövfall Nod ( x : Int , vänster : Träd [ T ], höger : Träd [ T ]) enum Form ( centerX : Int , centerY : Int ): case Square ( sida : Int , centerX : Int , centerY : Int ) förlänger Form ( centerY , centerX ) fall Rektangel ( längd : Int , höjd : Int , centerX : Int , centerY : Int ) förlänger Form ( centerX , centerY ) fall Cirkel ( radie : Int , centerX : Int ) , centerY : Int ) utökar formen ( centerX , centerY )
Språket Rust har omfattande stöd för taggade fackföreningar, kallade enums. Till exempel:
enum Träd { Blad , Nod ( i64 , Box < Träd > , Box < Träd > ) }
Det tillåter också matchning på fackföreningar:
0
0
låt träd = Träd :: Nod ( 2 , Box :: ny ( Träd :: Nod ( , Box :: ny ( Träd :: Leaf ), Box :: ny ( Träd :: Leaf ))), Box :: ny ( Träd :: Nod ( 3 , Box :: ny ( Träd :: Leaf ), Box :: ny ( Träd :: Nod ( 4 , Box :: ny ( Träd :: Leaf ), Box :: ny ( Träd :: Leaf ))))) ); fn add_values ( tree : Tree ) -> i64 { match tree { Tree :: Node ( v , a , b ) => v + add_values ( * a ) + add_values ( * b ), Tree :: Leaf => } } assert_eq ! ( add_values ( träd ), 9 );
Rusts felhanteringsmodell förlitar sig mycket på dessa taggade fackföreningar, särskilt Option<T>
-typen, som är antingen None
eller Some(T)
, och Result<T, E>
-typen, som är antingen Ok(T)
eller Err(E) )
.
Swift har också ett stort stöd för taggade fackföreningar via uppräkningar. Till exempel:
0
0
enum Träd { case leaf indirect case nod ( Int , Tree , Tree ) } låt träd = Träd . nod ( 2 , . nod ( , . blad , . blad ), . nod ( 3 , . blad , . nod ( 4 , . blad , . blad )) ) func add_values ( _ träd : Träd ) -> Int { byta träd { fall låt . nod ( v , a , b ): return v + add_values ( a ) + add_values ( b ) case . leaf : return } } assert ( add_values ( träd ) == 9 )
Med TypeScript är det möjligt att skapa taggade förbund också. Till exempel:
gränssnitt Leaf { kind : "leaf" ; } gränssnitt Node { kind : "nod" ; värde : antal ; vänster : Träd ; höger : Träd ; } typ Träd = Löv | Nodkonst rot : Träd = { typ : "nod" , värde : 5 , vänster : { typ : " nod" , värde : 1 , vänster : { typ : "löv" }, höger : { typ : "löv" } } , höger : { typ : "nod" , värde : 3 , vänster : { typ : "blad" }, höger : { typ : "nod" , värde : 4 , vänster : { typ : "löv" }, höger : { typ : "blad" } } } } funktionsbesök ( träd : träd ) { switch ( träd . typ ) { case "leaf" : break case " nod" : console . log ( träd . värde ) besök ( träd . vänster ) besök ( träd . höger ) bryta } }
Python 3.9 introducerar stöd för att skriva annoteringar som kan användas för att definiera en taggad fackföreningstyp (PEP-593):
Valuta = Annoterad [ TypedDict ( 'Currency' , { 'dollars' : float , 'pounds' : float }, total = False ), TaggedUnion , ]
C++17 introducerar std::variant och constexpr if
använder Tree = std :: variant < struct Leaf , struct Node > ; struct Leaf { std :: strängvärde ; _ }; struct Node { Träd * vänster = nullptr ; Träd * höger = nullptr ; }; struct Transverser { mall < typnamn T > void operator ()( T && v ) { if constexpr ( std :: is_same_v < T , Leaf &> ) { std :: cout << v . värde << " \n " ; } else if constexpr ( std :: is_same_v < T , Node &> ) { if ( v . left != nullptr ) std :: visit ( Transverser {}, * v . left ); if ( v . höger != nullptr ) std :: besök ( Transverser {}, * v . right ); } else { // Uttrycket !sizeof(T) är alltid false static_assert ( ! sizeof ( T ), "icke uttömmande besökare!" ); }; } }; /*Trädskog = ...; std::visit(Transverser{}, skog);*/
Klasshierarkier som taggade fackföreningar
I en typisk klasshierarki inom objektorienterad programmering kan varje underklass kapsla in data som är unika för den klassen. Metadata som används för att utföra virtuell metodsökning (till exempel objektets vtable -pekare i de flesta C++-implementeringar) identifierar underklassen och fungerar så effektivt som en tagg som identifierar den specifika data som lagras av instansen (se RTTI ). Ett objekts konstruktor ställer in denna tagg, och den förblir konstant under objektets livstid.
Ändå involverar en klasshierarki sann subtyppolymorfism ; den kan utökas genom att skapa ytterligare underklasser av samma bastyp, som inte kunde hanteras korrekt under en tagg/utskick-modell. Därför är det vanligtvis inte möjligt att göra ärendeanalys eller sändning på ett subobjekts "tagg" som man skulle göra för taggade fackföreningar. Vissa språk som Scala tillåter att basklasser "förseglas" och förenar taggade fackföreningar med förseglade basklasser.
Se även
- Discriminator , typtaggen för diskriminerade fackföreningar i CORBA
- Varianttyp (COM)
externa länkar
- boost::variant är en C++ typsäker diskriminerad union
- std.variant är en implementering av varianttyp i D 2.0