Haskell Language
Skriv ansökan
Sök…
Introduktion
TypeApplications
är ett alternativ till typanteckningar när kompilatorn kämpar för att dra slutsatser för ett visst uttryck.
Denna serie av exempel kommer att förklara syftet med tillägget TypeApplications
och hur man använder det
Glöm inte att aktivera tillägget genom att placera {-# LANGUAGE TypeApplications #-}
högst upp i din källfil.
Undvika anteckningar av typ
Vi använder typanteckningar för att undvika tvetydighet. Typapplikationer kan användas för samma ändamål. Till exempel
x :: Num a => a
x = 5
main :: IO ()
main = print x
Den här koden har ett tvetydighetsfel. Vi vet att a
har en Num
instans, och för att skriva ut den vet vi att den behöver en Show
instans. Detta kan fungera om a
till exempel var en Int
, så för att fixa felet kan vi lägga till en typanteckning
main = print (x :: Int)
En annan lösning med användning av typapplikationer skulle se ut så här
main = print @Int x
För att förstå vad detta betyder måste vi titta på typsignaturen på print
.
print :: Show a => a -> IO ()
Funktionen tar en parameter av typ a
, men ett annat sätt att titta på den är att den faktiskt tar två parametrar. Den första är en parameter typ, är den andra ett värde vars typ är den första parametern.
Den största skillnaden mellan värdeparametrar och typparametrarna är att de senare tillförs implicit till funktioner när vi kallar dem. Vem tillhandahåller dem? Typinferensalgoritmen! Vad TypeApplications
låter oss göra är att ge dessa typparametrar uttryckligen. Detta är särskilt användbart när typinferensen inte kan bestämma rätt typ.
Så för att dela upp ovanstående exempel
print :: Show a => a -> IO ()
print @Int :: Int -> IO ()
print @Int x :: IO ()
Skriv applikationer på andra språk
Om du känner till språk som Java, C # eller C ++ och konceptet med generik / mallar kan denna jämförelse vara användbar för dig.
Säg att vi har en generisk funktion i C #
public static T DoNothing<T>(T in) { return in; }
För att kalla denna funktion med en float
vi göra DoNothing(5.0f)
eller om vi vill vara tydliga kan vi säga DoNothing<float>(5.0f)
. Den delen inuti vinkelfästarna är typapplikationen.
I Haskell är det samma, förutom att typparametrarna inte bara är implicita på samtalssidor utan också på definitionssidor.
doNothing :: a -> a
doNothing x = x
Detta kan också göras tydligt med antingen ScopedTypeVariables
, Rank2Types
eller RankNTypes
tillägg som detta.
doNothing :: forall a. a -> a
doNothing x = x
På samtalssidan kan vi återigen antingen skriva doNothing 5.0
eller doNothing @Float 5.0
Parameterordning
Problemet med att typargument implicit blir uppenbart när vi har mer än ett. Vilken ordning kommer de in?
const :: a -> b -> a
Betyder att skriva const @Int
betyder a
är lika med Int
, eller är det b
? Om vi uttryckligen anger typparametrarna med hjälp av en forall
som const :: forall a b. a -> b -> a
då ordningen är som skriven: a
, sedan b
.
Om vi inte gör det, är variabelns ordning från vänster till höger. Den första variabeln som ska nämnas är den första typen av parametrar, den andra är den andra typen av parametrar och så vidare.
Vad händer om vi vill ange den andra typvariabeln, men inte den första? Vi kan använda ett jokertecken för den första variabeln som den här
const @_ @Int
Typen av detta uttryck är
const @_ @Int :: a -> Int -> a
Interaktion med tvetydiga typer
Anta att du introducerar en klass av typer som har en storlek i byte.
class SizeOf a where
sizeOf :: a -> Int
Problemet är att storleken ska vara konstant för varje värde av den typen. Vi vill faktiskt inte att funktionen sizeOf
ska bero på a
, utan bara av den typen.
Utan typapplikationer var den bästa lösningen vi hade den Proxy
typen som definierades så här
data Proxy a = Proxy
Syftet med denna typ är att bära typinformation, men ingen värdeinformation. Då kunde vår klass se ut så här
class SizeOf a where
sizeOf :: Proxy a -> Int
Nu undrar du kanske, varför inte släppa det första argumentet helt och hållet? Typen av vår funktion skulle då bara vara sizeOf :: Int
eller, för att vara mer exakt eftersom det är en metod för en klass, sizeOf :: SizeOf a => Int
eller för att vara ännu mer uttrycklig sizeOf :: forall a. SizeOf a => Int
.
Problemet är typinferens. Om jag skriver sizeOf
någonstans, vet inferensalgoritmen bara att jag förväntar mig en Int
. Det har ingen aning om vilken typ jag vill ersätta a
. På grund av detta avvisas definitionen av kompilatorn om du inte har {-# LANGUAGE AllowAmbiguousTypes #-}
aktiverat. I så fall kan definitionen sammanställas, den kan bara inte användas någonstans utan ett tvetydighetsfel.
Lyckligtvis sparar introduktionen av typapplikationer dagen! Nu kan vi skriva sizeOf @Int
, uttryckligen säga att a
är Int
. Typapplikationer tillåter oss att tillhandahålla en typparameter, även om den inte visas i funktionens faktiska parametrar !