Haskell Language
Type toepassing
Zoeken…
Invoering
TypeApplications
zijn een alternatief voor aantekeningen typen wanneer de compiler strijd te typen afleiden voor een bepaalde expressie.
Deze reeks voorbeelden legt het doel van de TypeApplications
extensie uit en hoe deze te gebruiken
Vergeet niet om de extensie in te schakelen door {-# LANGUAGE TypeApplications #-}
bovenaan uw bronbestand te plaatsen.
Type-annotaties vermijden
We gebruiken type-annotaties om dubbelzinnigheid te voorkomen. Type-applicaties kunnen voor hetzelfde doel worden gebruikt. Bijvoorbeeld
x :: Num a => a
x = 5
main :: IO ()
main = print x
Deze code bevat een dubbelzinnigheidsfout. We weten dat a
een Num
exemplaar heeft en om het af te drukken weten we dat het een Show
exemplaar nodig heeft. Dit zou kunnen werken als a
bijvoorbeeld een Int
, dus om de fout te verhelpen, kunnen we een type-annotatie toevoegen
main = print (x :: Int)
Een andere oplossing met typetoepassingen zou er zo uitzien
main = print @Int x
Om te begrijpen wat dit betekent, moeten we kijken naar de typeaanduiding van print
.
print :: Show a => a -> IO ()
De functie heeft één parameter van type a
, maar een andere manier om ernaar te kijken is dat er eigenlijk twee parameters nodig zijn. De eerste is een parameter type , de tweede is een waarde waarvan het type de eerste parameter is.
Het belangrijkste verschil tussen waardeparameters en de typeparameters is dat de laatste impliciet worden verstrekt aan functies wanneer we ze aanroepen. Wie biedt hen? Het type inferentie-algoritme! Wat TypeApplications
ons laten doen, is die TypeApplications
expliciet geven. Dit is vooral handig als de type-inferentie niet het juiste type kan bepalen.
Dus om het bovenstaande voorbeeld op te splitsen
print :: Show a => a -> IO ()
print @Int :: Int -> IO ()
print @Int x :: IO ()
Typ applicaties in andere talen
Als u bekend bent met talen zoals Java, C # of C ++ en het concept van generieke / sjablonen, dan is deze vergelijking misschien nuttig voor u.
Stel dat we een generieke functie hebben in C #
public static T DoNothing<T>(T in) { return in; }
Om deze functie met een float
te roepen, kunnen we DoNothing(5.0f)
of als we expliciet willen zijn, kunnen we DoNothing<float>(5.0f)
. Dat deel binnen de punthaken is de typetoepassing.
In Haskell is het hetzelfde, behalve dat de typeparameters niet alleen impliciet zijn op aanroepsites, maar ook op definitiesites.
doNothing :: a -> a
doNothing x = x
Dit kan ook expliciet worden gemaakt met behulp van deze ScopedTypeVariables
, Rank2Types
, Rank2Types
of RankNTypes
.
doNothing :: forall a. a -> a
doNothing x = x
Vervolgens kunnen we op de oproepsite opnieuw doNothing 5.0
of doNothing @Float 5.0
Volgorde van parameters
Het probleem met impliciete typeargumenten wordt duidelijk als we er meer dan één hebben. In welke volgorde komen ze binnen?
const :: a -> b -> a
Betekent het schrijven van const @Int
dat a
gelijk is aan Int
of is het b
? In het geval dat we expliciet de typeparameters vermelden met een forall
like const :: forall a b. a -> b -> a
dan is de volgorde zoals geschreven: a
, dan b
.
Als we dat niet doen, is de volgorde van variabelen van links naar rechts. De eerste variabele die moet worden genoemd, is de parameter van het eerste type, de tweede is de parameter van het tweede type, enzovoort.
Wat als we de tweede type variabele willen specificeren, maar niet de eerste? We kunnen een jokerteken gebruiken voor de eerste variabele zoals deze
const @_ @Int
Het type van deze uitdrukking is
const @_ @Int :: a -> Int -> a
Interactie met dubbelzinnige types
Stel dat u een klasse typen introduceert met een grootte in bytes.
class SizeOf a where
sizeOf :: a -> Int
Het probleem is dat de grootte voor elke waarde van dat type constant moet zijn. We willen eigenlijk niet dat de functie sizeOf
afhankelijk is van a
, maar alleen van het type.
Zonder typetoepassingen was de beste oplossing die we hadden het Proxy
type dat als volgt is gedefinieerd
data Proxy a = Proxy
Het doel van dit type is om type-informatie te dragen, maar geen waarde-informatie. Dan zou onze klas er zo uit kunnen zien
class SizeOf a where
sizeOf :: Proxy a -> Int
Nu vraag je je misschien af, waarom zou je het eerste argument niet helemaal laten vallen? Het type van onze functie zou dan gewoon sizeOf :: Int
of, om precies te zijn omdat het een methode van een klasse is, sizeOf :: SizeOf a => Int
of nog explicieter sizeOf :: forall a. SizeOf a => Int
.
Het probleem is type-gevolgtrekking. Als ik ergens ' sizeOf
schrijf, weet het inferentie-algoritme alleen dat ik een Int
verwacht. Het heeft geen idee welk type ik wil vervangen door a
. Hierdoor wordt de definitie door de compiler afgewezen, tenzij u de extensie {-# LANGUAGE AllowAmbiguousTypes #-}
ingeschakeld. In dat geval wordt de definitie gecompileerd, deze kan gewoon nergens worden gebruikt zonder een dubbelzinnigheidsfout.
Gelukkig bespaart de introductie van typetoepassingen de dag! Nu kunnen we sizeOf @Int
schrijven en expliciet zeggen dat a
Int
. Met typetoepassingen kunnen we een typeparameter opgeven, zelfs als deze niet voorkomt in de werkelijke parameters van de functie !