Type kruising - Intersection type
In typetheorie kan een kruispunttype worden toegekend aan waarden die zowel aan het type als aan het type kunnen worden toegekend . Deze waarde kan het type kruispunt worden gegeven in een systeem van het type kruispunt . In het algemeen, indien de bereiken van waarden van de twee typen overlappen, dan is een waarde die tot het snijpunt van de twee bereiken kunnen worden de toegewezen kruisingtype van deze twee typen. Een dergelijke waarde kan veilig als argument worden doorgegeven aan functies die een van de twee typen verwachten. In Java implementeert de klasse bijvoorbeeld zowel de interface als de interface. Daarom kan een object van het type veilig worden doorgegeven aan functies die een argument van het type verwachten en aan functies die een argument van het type verwachten .
BooleanSerializableComparableBooleanSerializableComparable
Typen kruispunten zijn samengestelde gegevenstypen . Net als bij producttypen worden ze gebruikt om verschillende typen aan een object toe te kennen. Producttypes worden echter aan tupels toegewezen , zodat aan elk tuple-element een bepaald producttypecomponent wordt toegewezen. Ter vergelijking: onderliggende objecten van typen kruispunten zijn niet noodzakelijkerwijs samengesteld. Een beperkte vorm van kruispunttypen zijn verfijningstypen .
Typen kruispunten zijn handig voor het beschrijven van overbelaste functies . Als bijvoorbeeld het type functie is dat een getal als argument neemt en een getal retourneert, en het type functie is dat een string als argument neemt en een string retourneert, dan kan het snijpunt van deze twee typen worden gebruikt om te beschrijven ( overbelast) functies die het een of het ander doen, op basis van het type invoer dat ze krijgen.
number => numberstring => string
Hedendaagse programmeertalen, waaronder Ceylon , Flow, Java , Scala , TypeScript en Whiley (zie vergelijking van talen met intersectietypes ), gebruiken intersectietypes om interfacespecificaties te combineren en ad-hocpolymorfisme uit te drukken . Als aanvulling op parametrisch polymorfisme kunnen intersectietypen worden gebruikt om vervuiling van de klassenhiërarchie door transversale problemen te voorkomen en boilerplate-code te verminderen , zoals weergegeven in het onderstaande TypeScript-voorbeeld .
De typetheoretische studie van kruispunttypes wordt de discipline van het kruispunttype genoemd . Opmerkelijk is dat programmabeëindiging nauwkeurig kan worden gekarakteriseerd met behulp van kruispunttypen.
TypeScript voorbeeld
TypeScript ondersteunt intersectietypen, waardoor de expressiviteit van het typesysteem wordt verbeterd en de potentiële klassenhiërarchie wordt verkleind, zoals als volgt wordt aangetoond.
De volgende programmacode definieert de klassen Chicken, Cow, en RandomNumberGeneratordie elk een methode hebben producedie een object van het type Egg, Milk, of retourneert number. Bovendien vereisen de functies eatEggen drinkMilkargumenten van het type Eggen Milkrespectievelijk .
class Egg { private kind: "Egg" }
class Milk { private kind: "Milk" }
//produces eggs
class Chicken { produce() { return new Egg(); } }
//produces milk
class Cow { produce() { return new Milk(); } }
//produces a random number
class RandomNumberGenerator { produce() { return Math.random(); } }
//requires an egg
function eatEgg(egg: Egg) {
return "I ate an egg.";
}
//requires milk
function drinkMilk(milk: Milk) {
return "I drank some milk.";
}
De volgende programmacode definieert de ad hoc polymorfe functie animalToFooddie de lidfunctie producevan het gegeven object aanroept animal. De functie animalToFoodheeft twee typeannotaties, namelijk en , verbonden via de intersectietype constructor . In het bijzonder, wanneer toegepast op een argument van type retourneert een object van type type , en wanneer toegepast op een argument van type retourneert een object van type type . Idealiter zou het niet van toepassing moeten zijn op enig object met (mogelijk toevallig) een methode.
((_: Chicken) => Egg)((_: Cow) => Milk)&animalToFoodChickenEggCowMilkanimalToFoodproduce
//given a chicken, produces an egg; given a cow, produces milk
let animalToFood: ((_: Chicken) => Egg) & ((_: Cow) => Milk) =
function (animal: any) {
return animal.produce();
};
Ten slotte demonstreert de volgende programmacode het typeveilig gebruik van de bovenstaande definities.
var chicken = new Chicken();
var cow = new Cow();
var randomNumberGenerator = new RandomNumberGenerator();
console.log(chicken.produce()); //Egg { }
console.log(cow.produce()); //Milk { }
console.log(randomNumberGenerator.produce()); //0.2626353555444987
console.log(animalToFood(chicken)); //Egg { }
console.log(animalToFood(cow)); //Milk { }
//console.log(animalToFood(randomNumberGenerator)); //ERROR: Argument of type 'RandomNumberGenerator' is not assignable to parameter of type 'Cow'
console.log(eatEgg(animalToFood(chicken))); //I ate an egg.
//console.log(eatEgg(animalToFood(cow))); //ERROR: Argument of type 'Milk' is not assignable to parameter of type 'Egg'
console.log(drinkMilk(animalToFood(cow))); //I drank some milk.
//console.log(drinkMilk(animalToFood(chicken))); //ERROR: Argument of type 'Egg' is not assignable to parameter of type 'Milk'
Bovenstaande programmacode heeft de volgende eigenschappen:
- Regels 1-3 maken objecten
chicken,cow, enrandomNumberGeneratorvan hun respectievelijke type. - Regels 5-7 drukken voor de eerder gemaakte objecten de respectieve resultaten af (meegeleverd als opmerkingen) bij het aanroepen van
produce. - Regel 9 (resp. 10) demonstreert type veilig gebruik van de methode
animalToFoodtoegepast opchicken(resp.cow). - Regel 11 zou, indien niet becommentarieerd, resulteren in een typefout tijdens het compileren. Hoewel de implementatie van
animalToFooddeproducemethode van zou kunnen aanroepenrandomNumberGenerator, staat de typeannotatie van ditanimalToFoodniet toe. Dit is in overeenstemming met de bedoelde betekenis vananimalToFood. - Regel 13 (resp. 15) laat zien dat toepassen
animalToFoodopchicken(resp.cow) resulteert in een object van het typeEgg(resp.Milk). - Regel 14 (resp. 16) laat zien dat toepassen
animalToFoodopcow(resp.chicken) niet resulteert in een object van het typeEgg(resp.Milk). Daarom zou regel 14 (resp. 16) zonder commentaar resulteren in een typefout tijdens het compileren.
Vergelijking met erfenis
Het bovenstaande minimalistische voorbeeld kan worden gerealiseerd met behulp van overerving , bijvoorbeeld door de klassen Chickenen Cowuit een basisklasse af te leiden Animal. In een grotere setting kan dit echter nadelig zijn. Het introduceren van nieuwe klassen in een klassenhiërarchie is niet noodzakelijkerwijs gerechtvaardigd vanwege transversale problemen , of is misschien ronduit onmogelijk, bijvoorbeeld bij het gebruik van een externe bibliotheek. Het is denkbaar dat het bovenstaande voorbeeld kan worden uitgebreid met de volgende klassen:
- een klasse
Horsedie geenproducemethode heeft; - een klasse
Sheepmet eenproducemethode die terugkeertWool; - een klasse
Pigdie eenproducemethode heeft die maar één keer kan worden gebruikt, met als resultaatMeat.
Dit kan aanvullende klassen (of interfaces) vereisen die specificeren of een productiemethode beschikbaar is, of de productiemethode voedsel retourneert en of de productiemethode herhaaldelijk kan worden gebruikt. Over het algemeen kan dit de klassenhiërarchie vervuilen.
Vergelijking met eend typen
Bovenstaand minimalistisch voorbeeld laat al zien dat het typen van eenden minder geschikt is om het gegeven scenario te realiseren. Hoewel de klasse RandomNumberGeneratoreen producemethode bevat , mag het object randomNumberGeneratorgeen geldig argument zijn voor animalToFood. Het bovenstaande voorbeeld kan worden gerealiseerd met behulp van duck-typering, bijvoorbeeld door een nieuw veld argumentForAnimalToFoodin de klassen te introduceren Chickenen aan te geven Cowdat objecten van het overeenkomstige type geldige argumenten zijn voor animalToFood. Dit zou echter niet alleen de grootte van de respectieve klassen vergroten (vooral met de introductie van meer methoden die vergelijkbaar zijn met animalToFood), maar het is ook een niet-lokale benadering met betrekking tot animalToFood.
Vergelijking met functie overbelasting
Het bovenstaande voorbeeld kan worden gerealiseerd door functieoverbelasting te gebruiken , bijvoorbeeld door twee methoden te implementeren en . In TypeScript is een dergelijke oplossing bijna identiek aan het gegeven voorbeeld. Andere programmeertalen, zoals Java , vereisen verschillende implementaties van de overbelaste methode. Dit kan leiden tot ofwel code duplicatie of standaardtekst code .
animalToFood(animal: Chicken): EgganimalToFood(animal: Cow): Milk
Vergelijking met het bezoekerspatroon
Bovenstaand voorbeeld kan worden gerealiseerd met behulp van het bezoekerspatroon . Elke dierklasse zou een acceptmethode moeten implementeren die een object accepteert dat de interface implementeert AnimalVisitor(door niet-lokale standaardcode toe te voegen ). De functie animalToFoodzou worden gerealiseerd als de visitmethode van een implementatie van AnimalVisitor. Helaas zou de verbinding tussen het invoertype ( Chickenof Cow) en het resultaattype ( Eggof Milk) moeilijk te representeren zijn.
Beperkingen
Aan de ene kant, kruising types kunnen worden gebruikt om lokaal annoteren verschillende typen om een functie zonder het introduceren van nieuwe klassen (of interfaces) aan de klas hiërarchie. Aan de andere kant vereist deze benadering dat alle mogelijke argumenttypes en resultaattypes expliciet worden gespecificeerd. Als het gedrag van een functie precies kan worden gespecificeerd door een uniforme interface, parametrisch polymorfisme of duck-typering , dan is de uitgebreide aard van intersectietypen ongunstig. Daarom moeten kruispunttypen worden beschouwd als complementair aan bestaande specificatiemethoden.
Afhankelijk type kruispunt
Een afhankelijk intersectietype , aangeduid , is een afhankelijk type waarbij het type afhankelijk kan zijn van de term variabele . Met name wanneer een term heeft de afhankelijke kruisingtype , wordt de term heeft zowel het type en het type , waarbij de type dat voortvloeit uit het vervangen van alle voorkomens van de term variabele in de term .
Scala voorbeeld
Scala ondersteunt typedeclaraties als objectleden. Hierdoor kan een type van een objectlid afhankelijk zijn van de waarde van een ander lid, wat een padafhankelijk type wordt genoemd . De volgende programmatekst definieert bijvoorbeeld een Scala-eigenschap Witness, die kan worden gebruikt om het singleton-patroon te implementeren .
trait Witness {
type T
val value: T {}
}
De bovenstaande eigenschap Witnessdeclareert het lid T, waaraan een type kan worden toegewezen als zijn waarde, en het lid value, dat een waarde van type kan worden toegewezen T. De volgende programmatekst definieert een object booleanWitnessals instantie van de bovenstaande eigenschap Witness. Het object booleanWitnessdefinieert het type Tals Booleanen de waarde valueals true. Bijvoorbeeld afdrukken uitvoeren op de console.
System.out.println(booleanWitness.value)true
object booleanWitness extends Witness {
type T = Boolean
val value = true
}
Laat het type zijn (in het bijzonder een recordtype ) van objecten met het lid type . In het bovenstaande voorbeeld kan aan het object het afhankelijke type kruising worden toegewezen . De redenering is als volgt. Het object heeft het lid waaraan het type is toegewezen als zijn waarde. Aangezien het een type is, heeft het object het type . Bovendien heeft het object het lid waaraan de waarde type is toegewezen . Aangezien de waarde van is , heeft het object het type . Over het algemeen heeft het object het type kruising . Daarom, door zelfreferentie als afhankelijkheid te presenteren, heeft het object het afhankelijke intersectietype .
booleanWitnessbooleanWitnessTBooleanBooleanbooleanWitnessbooleanWitnessvaluetrueBooleanbooleanWitness.TBooleanbooleanWitnessbooleanWitnessbooleanWitness
Als alternatief kan het bovenstaande minimalistische voorbeeld worden beschreven met behulp van afhankelijke recordtypen . In vergelijking met afhankelijke typen kruispunten vormen afhankelijke recordtypen een strikt meer gespecialiseerd typetheoretisch concept.
Snijpunt van een typefamilie
Een intersectie van een typefamilie , aangeduid als , is een afhankelijk type waarbij het type afhankelijk kan zijn van de term variabele . Met name wanneer een term heeft het type en daarna gedurende elke periode van het type , de term heeft soort . Dit begrip wordt ook wel impliciet Pi-type genoemd , waarbij wordt opgemerkt dat het argument niet op termniveau wordt gehouden.
Vergelijking van talen met typen kruispunten
| Taal | Actief ontwikkeld | paradigma(s) | Toestand | Functies |
|---|---|---|---|---|
| C# | Ja | Onder discussie | ? | |
| Ceylon | Ja | Ondersteund |
|
|
| F# | Ja | Onder discussie | ? | |
| Stromen | Ja | Ondersteund |
|
|
| Forsythe | Nee | Ondersteund |
|
|
| Java | Ja | Ondersteund |
|
|
| PHP | Ja | Ondersteund |
|
|
| Scala | Ja | Ondersteund |
|
|
| TypeScript | Ja | Ondersteund |
|
|
| Whiley | Ja | Ondersteund | ? |