Elm Language
Typen, typevariabelen en typeconstructeurs
Zoeken…
Opmerkingen
Speel alsjeblieft zelf met deze concepten om ze echt onder de knie te krijgen! De elm-repl (zie de inleiding van de REPL ) is waarschijnlijk een goede plek om met de bovenstaande code te spelen. Je kunt ook online met elm-repl .
Vergelijkbare gegevenstypen
Vergelijkbare primitieve types die kunnen worden vergeleken met behulp van vergelijkingsoperators Basics module, zoals: (<) , (>) , (<=) , (>=) , max , min , compare
Vergelijkbare types in Elm zijn Int , Float , Time , Char , String en tuples of lijsten met vergelijkbare types.
In documentatie of typedefinities worden ze aangeduid als een comparable speciale comparable , bijv. zie Basics.max voor Basics.max functie:
max : comparable -> comparable -> comparable
Typ handtekeningen
In Elm worden waarden aangegeven door een naam, een is-gelijk-teken te schrijven en vervolgens de werkelijke waarde:
someValue = 42
Functies zijn ook waarden, met de toevoeging van een waarde of waarden als argumenten. Ze worden meestal als volgt geschreven:
double n = n * 2
Elke waarde in Elm heeft een type. De typen van de bovenstaande waarden worden door de compiler afgeleid, afhankelijk van hoe ze worden gebruikt. Het is echter best gebruikelijk om altijd expliciet het type van een waarde op het hoogste niveau te declareren en om dit te doen, schrijft u een typeaanduiding als volgt:
someValue : Int
someValue =
42
someOtherValue : Float
someOtherValue =
42
Zoals we kunnen zien, 42 kan worden gedefinieerd als een Int of Float . Dit is intuïtief logisch, maar zie Type Variabelen voor meer informatie.
Type handtekeningen zijn bijzonder waardevol bij gebruik met functies. Hier is de verdubbelingsfunctie van eerder:
double : Int -> Int
double n =
n * 2
Deze keer heeft de handtekening een -> , een pijl en spreken we de handtekening uit als "int to int", of "neemt een geheel getal en retourneert een geheel getal". -> geeft aan dat door double een Int waarde als argument te geven, double een Int zal retourneren. Daarom is een geheel getal nodig voor een geheel getal:
> double
<function> : Int -> Int
> double 3
6 : Int
Basistypen
Typ in elm-repl een stuk code om de waarde en het afgeleide type te krijgen. Probeer het volgende om meer te weten te komen over de verschillende typen die bestaan:
> 42
42 : number
> 1.987
1.987 : Float
> 42 / 2
21 : Float
> 42 % 2
0 : Int
> 'e'
'e' : Char
> "e"
"e" : String
> "Hello Friend"
"Hello Friend" : String
> ['w', 'o', 'a', 'h']
['w', 'o', 'a', 'h'] : List Char
> ("hey", 42.42, ['n', 'o'])
("hey", 42.42, ['n', 'o']) : ( String, Float, List Char )
> (1, 2.1, 3, 4.3, 'c')
(1,2.1,3,4.3,'c') : ( number, Float, number', Float, Char )
> {}
{} : {}
> { hey = "Hi", someNumber = 43 }
{ hey = "Hi", someNumber = 43 } : { hey : String, someNumber : number }
> ()
() : ()
{} is het lege recordtype en () is het lege Tuple-type. De laatste wordt vaak gebruikt voor luie evaluatie. Zie het overeenkomstige voorbeeld in Functies en gedeeltelijke toepassing .
Merk op hoe het number hoofdletter lijkt. Dit geeft aan dat het een type variabele, en bovendien het bepaald woord number verwijst naar een speciaal type variabele die ofwel een kan Int of Float (zie de overeenkomstige secties voor meer). Typen zijn echter altijd hoofdletters, zoals Char , Float , List String , enzovoort.
Type variabelen
Type variabelen zijn niet-gekapitaliseerde namen in type-handtekeningen. In tegenstelling tot hun gekapitaliseerde tegenhangers, zoals Int en String , vertegenwoordigen ze geen enkel type, maar eerder elk type. Ze worden gebruikt om generieke functies te schrijven die op elk type of type kunnen werken, en zijn met name handig voor het schrijven van bewerkingen boven containers zoals List of Dict . De functie List.reverse heeft bijvoorbeeld de volgende handtekening:
reverse : List a -> List a
Wat betekent dat het kan werken op een lijst met elke typewaarde , dus List Int , List (List String) , beide en andere kunnen allemaal toch worden reversed . Daarom is a een typevariabele die voor elk type kan gelden.
De reverse functie had elke naam zonder hoofdletter in de typeaanduiding kunnen gebruiken, behalve het handjevol speciale typen variabelenamen , zoals het number (zie het overeenkomstige voorbeeld voor meer informatie):
reverse : List lol -> List lol
reverse : List wakaFlaka -> List wakaFlaka
De namen van het type variabelen worden alleen zinvol als er als er verschillende soorten variabelen binnen een enkele handtekening, wordt geïllustreerd door de map functie op lijsten:
map : (a -> b) -> List a -> List b
map neemt een functie van elk type a tot elk type b , samen met een lijst met elementen van een type a , en retourneert een lijst met elementen van een type b , die het krijgt door de gegeven functie toe te passen op elk element van de lijst.
Laten we de handtekening concreet maken om dit beter te kunnen zien:
plusOne : Int -> Int
plusOne x =
x + 1
> List.map plusOne
<function> : List Int -> List Int
Zoals we kunnen zien, zijn in dit geval zowel a = Int als b = Int . Maar als map een typeaanduiding zoals map : (a -> a) -> List a -> List a , dan zou het alleen werken op functies die op één type werken, en je zou nooit in staat zijn om de type van een lijst met behulp van de map functie. Maar omdat de typeaanduiding van de map meerdere verschillende typevariabelen heeft, a en b , kunnen we map gebruiken om het type van een lijst te wijzigen:
isOdd : Int -> Bool
isOdd x =
x % 2 /= 0
> List.map isOdd
<function> : List Int -> List Bool
In dit geval zijn a = Int en b = Bool . Daarom moet u verschillende typevariabelen gebruiken om functies te kunnen gebruiken die verschillende typen kunnen gebruiken en retourneren.
Typ aliassen
Soms willen we een type een meer beschrijvende naam geven. Stel dat onze app een gegevenstype heeft dat gebruikers vertegenwoordigt:
{ name : String, age : Int, email : String }
En onze functies voor gebruikers hebben typeaanduidingen in de trant van:
prettyPrintUser : { name : String, age : Int, email : String } -> String
Dit kan behoorlijk onhandig worden met een groter recordtype voor een gebruiker, dus laten we een type-alias gebruiken om de grootte te verkleinen en een meer betekenisvolle naam aan die gegevensstructuur te geven:
type alias User =
{ name: String
, age : Int
, email : String
}
prettyPrintUser : User -> String
Type-aliassen maken het veel schoner om een model voor een toepassing te definiëren en te gebruiken:
type alias Model =
{ count : Int
, lastEditMade : Time
}
type alias letterlijk alleen aliassen van een type met de naam die u eraan geeft. Model bovenstaande { count : Int, lastEditMade : Time } is precies hetzelfde als { count : Int, lastEditMade : Time } . Hier is een voorbeeld dat laat zien hoe aliassen niet verschillen van de onderliggende typen:
type alias Bugatti = Int
type alias Fugazi = Int
unstoppableForceImmovableObject : Bugatti -> Fugazi -> Int
unstoppableForceImmovableObject bug fug =
bug + fug
> unstoppableForceImmovableObject 09 87
96 : Int
Een type-alias voor een recordtype definieert een constructorfunctie met één argument voor elk veld in declaratievolgorde.
type alias Point = { x : Int, y : Int }
Point 3 7
{ x = 3, y = 7 } : Point
type alias Person = { last : String, middle : String, first : String }
Person "McNameface" "M" "Namey"
{ last = "McNameface", middle = "M", first = "Namey" } : Person
Elk alias van het recordtype heeft zijn eigen veldvolgorde, zelfs voor een compatibel type.
type alias Person = { last : String, middle : String, first : String }
type alias Person2 = { first : String, last : String, middle : String }
Person2 "Theodore" "Roosevelt" "-"
{ first = "Theodore", last = "Roosevelt", middle = "-" } : Person2
a = [ Person "Last" "Middle" "First", Person2 "First" "Last" "Middle" ]
[{ last = "Last", middle = "Middle", first = "First" },{ first = "First", last = "Last", middle = "Middle" }] : List Person2
Type-veiligheid verbeteren met behulp van nieuwe typen
Aliasingtypen verminderen de boilerplate en verbeteren de leesbaarheid, maar het is niet type-veiliger dan het alias type zelf is. Stel je de volgende situatie voor:
type alias Email = String
type alias Name = String
someEmail = "[email protected]"
someName = "Benedict"
sendEmail : Email -> Cmd msg
sendEmail email = ...
Met behulp van de bovenstaande code kunnen we sendEmail someName schrijven, en het zal compileren, hoewel het eigenlijk niet zou moeten, omdat ondanks namen en e-mails beide String 's zijn, het totaal verschillende dingen zijn.
We kunnen de ene String echt onderscheiden van een andere String op type-niveau door een nieuw type te maken . Hier is een voorbeeld dat Email herschrijft als een type plaats van een type alias :
module Email exposing (Email, create, send)
type Email = EmailAddress String
isValid : String -> Bool
isValid email =
-- ...validation logic
create : String -> Maybe Email
create email =
if isValid email then
Just (EmailAddress email)
else
Nothing
send : Email -> Cmd msg
send (EmailAddress email) = ...
Onze isValid functie doet iets om te bepalen of een string een geldig e-mailadres is. De create functie controleert of een gegeven String een geldige e-mail is en retourneert een Maybe verpakte Email om ervoor te zorgen dat we alleen gevalideerde adressen retourneren. Hoewel we de validatiecontrole kunnen omzeilen door een Email rechtstreeks te construeren door EmailAddress "somestring" , als onze moduleverklaring de constructor EmailAddress niet blootlegt, zoals hier wordt getoond
module Email exposing (Email, create, send)
dan heeft geen enkele andere module toegang tot de constructor EmailAddress , hoewel ze het Email EmailAddress nog steeds in annotaties kunnen gebruiken. De enige manier om een nieuwe Email buiten deze module te bouwen, is door de functie create die deze biedt, en die functie zorgt ervoor dat deze in de eerste plaats alleen geldige e-mailadressen retourneert. Daarom leidt deze API de gebruiker automatisch naar het juiste pad via het type veiligheid: send werkt alleen met waarden geconstrueerd door create , die een validatie uitvoert en de afhandeling van ongeldige e-mails afdwingt omdat het een Maybe Email retourneert.
Als u wilt om de export Email constructeur, kan je schrijft
module Email exposing (Email(EmailAddress), create, send)
Nu kan elk bestand dat Email importeert ook zijn constructor importeren. In dit geval kunnen gebruikers hiermee validatie omzeilen en ongeldige e-mails send , maar u bouwt niet altijd een API zoals deze, dus exporteurs kunnen nuttig zijn. Met een type dat meerdere constructors heeft, wilt u misschien ook slechts enkele van hen exporteren.
Constructiesoorten
De combinatie van het type alias trefwoord geeft een nieuwe naam voor een type, maar het type trefwoord in afzondering verklaart een nieuw type. Laten we een van de meest fundamentele van deze typen onderzoeken: Maybe
type Maybe a
= Just a
| Nothing
Het eerste dat moet worden opgemerkt, is dat het Maybe type wordt gedeclareerd met een typevariabele van a . Het tweede ding om op te merken is het pijpteken, | , wat "of" betekent. Met andere woorden, iets van het type Maybe a ofwel Just a of Nothing .
Wanneer u de bovenstaande code schrijft, komen Just and Nothing in het bereik als waarde-constructors en Maybe komt in het bereik als een type-constructor . Dit zijn hun handtekeningen:
Just : a -> Maybe a
Nothing : Maybe a
Maybe : a -> Maybe a -- this can only be used in type signatures
Vanwege de typevariabele a kan elk type worden "ingepakt" van het Maybe type. Dus, Maybe Int , Maybe (List String) of Maybe (Maybe (List Html)) , zijn allemaal geldige typen. Bij het vernietigen van een type met een case expressie, moet u rekening houden met elke mogelijke instantie van dat type. In het geval van een waarde van het type Maybe a , moet u zowel het geval Just a geval Nothing verklaren:
thing : Maybe Int
thing =
Just 3
blah : Int
blah =
case thing of
Just n ->
n
Nothing ->
42
-- blah = 3
Probeer het schrijven van de bovenstaande code zonder Nothing clausule in het case uitdrukking: het zal niet compileren. Dit maakt de Maybe een geweldig patroon voor het uitdrukken van waarden die misschien niet bestaan, omdat het je dwingt om de logica aan te pakken wanneer de waarde Nothing .
Het Never-type
De Never type kan niet worden gebouwd (de Basics module heeft zijn waarde niet constructeur uitgevoerd en heeft u er geen andere functie die terugkeert gegeven Never ofwel). Er is geen waarde never : Never of een functie createNever : ?? -> Never .
Dit heeft zijn voordelen: u kunt in een type systeem een mogelijkheid coderen die niet kan gebeuren. Dit kan worden gezien in typen zoals Task Never Int die garandeert dat het zal slagen met een Int ; of Program Never dat geen parameters nodig heeft bij het initialiseren van de Elm-code van JavaScript.
Speciale type variabelen
Elm definieert de volgende speciale typevariabelen die een bepaalde betekenis hebben voor de compiler:
comparable: bestaat uitInt,Float,Char,Stringen tupels daarvan. Dit maakt het gebruik van de operatoren<en>mogelijk.Voorbeeld: u kunt een functie definiëren om de kleinste en grootste elementen in een lijst (
extent) te vinden. Je denkt welk type handtekening je moet schrijven. Aan de ene kant kun jeextentInt : List Int -> Maybe (Int, Int)enextentChar : List Char -> Maybe (Char, Char)en nog een voorFloatenString. De implementatie hiervan zou hetzelfde zijn:extentInt list = let helper x (minimum, maximum) = ((min minimum x), (max maximum x)) in case list of [] -> Nothing x :: xs -> Just <| List.foldr helper (x, x) xsU kunt in de verleiding komen om eenvoudig de
extent : List a -> Maybe (a, a)te schrijvenextent : List a -> Maybe (a, a), maar de compiler laat u dit niet doen, omdat de functiesminenmaxniet zijn gedefinieerd voor deze typen (NB: dit zijn slechts eenvoudige wrappers rond de hierboven genoemde<-operator). U kunt dit oplossen door deextent : List comparable -> Maybe (comparable, comparable)definiërenextent : List comparable -> Maybe (comparable, comparable). Hierdoor kan uw oplossing polymorf zijn , wat betekent dat het voor meer dan één type werkt.number: bestaat uitIntenFloat. Staat het gebruik van rekenkundige operatoren toe behalve delen. U kunt dan bijvoorbeeldsum : List number -> numberdefiniërensum : List number -> numberen laten werken voor zowel ints als floats.appendable: Bestaat uitString,List. Staat het gebruik van de++operator toe.compappend: dit verschijnt soms, maar is een implementatiedetail van de compiler. Momenteel kan dit niet worden gebruikt in uw eigen programma's, maar wordt soms genoemd.
Merk op dat in een type-annotatie als deze: number -> number -> number deze allemaal naar hetzelfde type verwijzen, dus het doorgeven van Int -> Float -> Int zou een typefout zijn. U kunt dit oplossen door een achtervoegsel toe te voegen aan de naam van de typevariabele: number -> number' -> number'' zou dan prima compileren.
Hier is geen officiële naam voor, ze worden soms genoemd:
- Speciale type variabelen
- Typeklasse-achtige typevariabelen
- Pseudo-typeclasses
Dit komt omdat ze werken zoals Haskell's Type Classes , maar zonder de mogelijkheid voor de gebruiker om deze te definiëren.