Typ familj
Inom datavetenskap associerar en typfamilj datatyper med andra datatyper , med hjälp av en funktion på typnivå definierad av en öppen samling av giltiga instanser av indatatyper och motsvarande utdatatyper.
Typfamiljer är en funktion i vissa typsystem som tillåter att delfunktioner mellan typer definieras genom mönstermatchning . Detta till skillnad från datatypskonstruktörer , som definierar injektiva funktioner från alla typer av en viss typ till en ny uppsättning typer, och typsynonymer (aka typedef ), som definierar funktioner från alla typer av en viss typ till en annan befintlig uppsättning av typer med ett enda fall.
Typfamiljer och typklasser är nära besläktade: normaltypklasser definierar delfunktioner från typer till en samling namngivna värden genom mönstermatchning på indatatyperna, medan typfamiljer definierar partiella funktioner från typer till typer genom mönstermatchning på indatatyperna. Faktum är att i många användningar av typfamiljer finns det en enda typklass som logiskt innehåller både värden och typer som är associerade med varje instans. En typfamilj som deklareras i en typklass kallas en associerad typ .
Programmeringsspråk med stöd för typfamiljer eller liknande funktioner inkluderar Haskell (med en gemensam språktillägg), Standard ML (genom dess modulsystem), Scala (under namnet "abstrakta typer") och C++ (genom användning av typedefs i mallar) .
Variationer
TypeFamilies i Glasgow Haskell-kompilatorn stöder
både typsynonymfamiljer och datafamiljer . Typsynonymfamiljer är den mer flexibla (men svårare att typkontrollera) formen, vilket tillåter att typerna i typfunktionens koddomän är vilken typ som helst med lämplig typ . Datafamiljer, å andra sidan, begränsar koddomänen genom att kräva att varje instans definierar en ny typkonstruktor för funktionens resultat. Detta säkerställer att funktionen är injektiv , vilket gör att klienternas sammanhang kan dekonstruera typfamiljen och få den ursprungliga argumenttypen.
Motivation och exempel
Typfamiljer är användbara för att abstrahera mönster där en gemensam "organisation" eller "struktur" av typer upprepas, men med olika specifika typer i varje fall. Typiska användningsfall inkluderar att beskriva abstrakta datatyper som generiska samlingar eller designmönster som modell–vy–kontroller .
Självoptimerande abstrakta datatyper
En av de ursprungliga motiven för introduktionen av associerade typer var att tillåta abstrakta datatyper att parametriseras efter deras innehållstyp så att datastrukturen som implementerar den abstrakta typen varierar på ett "självoptimerande" sätt. Normala algebraiska datatypparametrar kan bara beskriva datastrukturer som beter sig enhetligt med avseende på alla argumenttyper. Associerade typer kan dock beskriva en familj av datastrukturer som har ett enhetligt gränssnitt men varierar i implementering enligt en eller flera typparametrar. Till exempel, med hjälp av Haskells associerade typer notation, kan vi deklarera en typklass av giltiga arrayelementtyper , med en associerad datafamilj som representerar en array av den elementtypen:
klass ArrayElem e där data Array e index :: Array e -> Int -> e
Förekomster kan sedan definieras för denna klass, som definierar både datastrukturen som används och operationerna på datastrukturen på en enda plats. För effektivitet kan vi använda en vektorrepresentation med packade bitar för arrayer av booleska värden, medan vi använder en normal arraydatastruktur för heltalsvärden. Datastrukturen för arrayer av ordnade par definieras rekursivt som ett par arrayer av var och en av elementtyperna.
instans ArrayElem Bool där data Array Bool = BoolArray BitVector index ( BoolArray ar ) i = indexBitVector ar i instans ArrayElem Int där data Array Int = IntArray UIntArr index ( IntArray ar ) i = indexUIntArr ar i instans ( ArrayElem a instans , Array> Elem a ) ArrayElem ( a , b ) där data Array ( a , b ) = PairArray ( Array a ) ( Array b ) index ( PairArray ar br ) = ( index ar i , index br i )
Med dessa definitioner, när en klient refererar till en Array (Int, Bool)
väljs en implementering automatiskt med de definierade instanserna.
En klass för samlingar
Om vi inverterar det föregående exemplet kan vi också använda typfamiljer för att definiera en klass för samlingstyper, där typfunktionen mappar varje samlingstyp till dess motsvarande elementtyp:
klass Samlar c där typ Elem c tom :: c infoga :: Elem c -> c -> c toList :: c -> [ Elem c ] instans Samlar [ e ] där typ Elem [ e ] = e tom = [] infoga = ( : ) toList = id instans Ord e => Samlar ( Set . Set e ) där typ Elem ( Set . Set e ) = e tom = Set . tom infoga = Set . insert toList = Set . att lista
I det här exemplet är användningen av en typsynonymfamilj i stället för en datafamilj viktig, eftersom flera samlingstyper kan ha samma elementtyp.
Jämförelse med funktionella beroenden
Funktionella beroenden är en annan typ av systemfunktion som har liknande användningsområden som associerade typer. Medan en associerad typ lägger till en namngiven typfunktion som mappar den omslutande typklassens parametrar till en annan typ, listar ett funktionellt beroende resultattypen som en annan parameter av typklassen och lägger till en begränsning mellan typparametrarna (t.ex. "parameter a bestämmer unikt parameter b ", skrivet a -> b
). De vanligaste användningarna av funktionella beroenden kan direkt konverteras till associerade typer och vice versa.
Typfamiljer anses generellt vara lättare att typkontrollera än funktionella beroenden. En annan fördel med associerade typer framför funktionella beroenden är att de senare kräver att klienter som använder typklassen anger alla beroende typer i sina sammanhang, inklusive de som de inte använder; eftersom associerade typer inte kräver detta kräver att lägga till en annan associerad typ till klassen att endast klassens instanser uppdateras, medan klienter kan förbli oförändrade. De huvudsakliga fördelarna med funktionella beroenden jämfört med typfamiljer är deras ökade flexibilitet vid hantering av några ovanliga fall.