Elm Language
Typen, Typvariablen und Typkonstruktoren
Suche…
Bemerkungen
Bitte spielen Sie selbst mit diesen Konzepten, um sie wirklich zu meistern! Der elm-repl (siehe Einleitung zur REPL ) ist wahrscheinlich ein guter Ort, um mit dem obigen Code elm-repl . Sie können auch online mit elm-repl .
Vergleichbare Datentypen
Vergleichbare Typen sind Grundtypen, die mit Vergleichsoperatoren aus dem Basics- Modul verglichen werden können, wie: (<) , (>) , (<=) , (>=) , max , min , compare
Vergleichbare Typen in Elm sind Int , Float , Time , Char , String und Tupel oder Listen vergleichbarer Typen.
In Dokumentationen oder Typdefinitionen werden sie als comparable spezielle Typvariable bezeichnet, z. Siehe Typdefinition für die Basics.max Funktion:
max : comparable -> comparable -> comparable
Typ Unterschriften
In Elm werden Werte durch Schreiben eines Namens, eines Gleichheitszeichens und dann des tatsächlichen Werts deklariert:
someValue = 42
Funktionen sind auch Werte, wobei zusätzlich ein Wert oder Werte als Argumente verwendet werden. Sie werden normalerweise wie folgt geschrieben:
double n = n * 2
Jeder Wert in Elm hat einen Typ. Die Typen der oben beschriebenen Werte werden durch den Compiler abgeleitet werden , je nachdem , wie sie verwendet werden. Es empfiehlt sich jedoch, den Typ eines Wertes der obersten Ebene immer explizit anzugeben. Dazu schreiben Sie eine Typensignatur wie folgt:
someValue : Int
someValue =
42
someOtherValue : Float
someOtherValue =
42
Wie wir sehen können, kann 42 entweder als Int oder als Float . Dies ist intuitiv sinnvoll. Weitere Informationen finden Sie unter Typvariablen .
Typunterschriften sind besonders nützlich, wenn sie mit Funktionen verwendet werden. Hier ist die Verdopplungsfunktion von vor:
double : Int -> Int
double n =
n * 2
Diesmal hat die Signatur ein -> , einen Pfeil, und wir würden die Signatur als "int bis int" aussprechen oder "eine ganze Zahl nehmen und eine ganze Zahl zurückgeben". -> gibt an, dass , indem sie double einen Int - Wert als Argument, double wird eine Rückkehr Int . Daher braucht es eine ganze Zahl zu einer ganzen Zahl:
> double
<function> : Int -> Int
> double 3
6 : Int
Grundtypen
elm-repl in elm-repl einen Code ein, um den Wert und den abgeleiteten Typ elm-repl . Versuchen Sie Folgendes, um die verschiedenen Typen kennenzulernen:
> 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 }
> ()
() : ()
{} ist der leere Record-Typ und () ist der leere Tuple-Typ. Letzteres wird häufig zum Zweck der faulen Bewertung verwendet. Siehe das entsprechende Beispiel in Funktionen und Teilanwendung .
Beachten Sie, wie die number aktiviert erscheint. Dies zeigt an, dass es sich um eine Art veränderlich ist, und darüber hinaus die bestimmte number bezieht sich auf eine spezielle Art Variable , die entweder eine sein , Int oder ein Float (siehe entsprechende Abschnitte für mehr). Typen sind jedoch immer Großbuchstaben wie Char , Float , List String usw.
Typvariablen
Typvariablen sind nicht kapitalisierte Namen in Typensignaturen. Im Gegensatz zu ihren kapitalisierten Gegenstücken, wie z. B. Int und String , repräsentieren sie nicht einen einzigen Typ, sondern einen beliebigen Typ. Sie werden verwendet, um generische Funktionen zu schreiben, die für jeden Typ und jeden Typ geeignet sind, und sind besonders nützlich, wenn Operationen über Container wie List oder Dict . Die Funktion List.reverse hat beispielsweise die folgende Signatur:
reverse : List a -> List a
Dies bedeutet, dass eine Liste mit einem beliebigen Typwert verwendet werden kann. Daher können List Int , List (List String) sowohl von diesen als auch von allen anderen reversed . Daher ist a eine Typvariable, die für jeden Typ stehen kann.
Die reverse Funktion haben könnte jeden uncapitalized Variablennamen in seiner Art Unterschrift, mit Ausnahme der wenige speziellen Typ Variablennamen, wie verwendet number (siehe das entsprechende Beispiel auf , dass für weitere Informationen):
reverse : List lol -> List lol
reverse : List wakaFlaka -> List wakaFlaka
Die Namen von Typvariablen werden nur dann sinnvoll, wenn innerhalb einer einzelnen Signatur verschiedene Typvariablen vorhanden sind, beispielsweise durch die map Funktion in Listen:
map : (a -> b) -> List a -> List b
map nimmt eine Funktion von einem beliebigen Typ a an einen beliebigen Typ b sowie eine Liste mit Elementen eines Typs a und gibt eine Liste von Elementen eines Typs b , die durch Anwenden der angegebenen Funktion auf jedes Element der Liste abgerufen wird.
Lassen Sie uns die Signatur konkretisieren, um das besser zu sehen:
plusOne : Int -> Int
plusOne x =
x + 1
> List.map plusOne
<function> : List Int -> List Int
Wie wir sehen können, sind in diesem Fall sowohl a = Int als auch b = Int . Wenn map jedoch eine Typensignatur wie map : (a -> a) -> List a -> List a , funktioniert sie nur mit Funktionen, die nur einen einzigen Typ betreffen, und Sie können den Typ niemals ändern Typ einer Liste mithilfe der map . Da die Typensignatur von map mehrere verschiedene Typvariablen hat, a und b , können Sie map , um den Typ einer Liste zu ändern:
isOdd : Int -> Bool
isOdd x =
x % 2 /= 0
> List.map isOdd
<function> : List Int -> List Bool
In diesem Fall ist a = Int und b = Bool . Damit Sie Funktionen verwenden können, die verschiedene Typen annehmen und zurückgeben können, müssen Sie verschiedene Typvariablen verwenden.
Geben Sie Aliase ein
Manchmal möchten wir einem Typ einen aussagekräftigeren Namen geben. Nehmen wir an, unsere App hat einen Datentyp, der Benutzer repräsentiert:
{ name : String, age : Int, email : String }
Und unsere Funktionen für Benutzer haben folgende Typunterschriften:
prettyPrintUser : { name : String, age : Int, email : String } -> String
Dies könnte bei einem größeren Datensatztyp für einen Benutzer recht unhandlich werden. Verwenden Sie also einen Typalias , um die Größe zu reduzieren und dieser Datenstruktur einen aussagekräftigeren Namen zu geben:
type alias User =
{ name: String
, age : Int
, email : String
}
prettyPrintUser : User -> String
Typ-Aliasnamen erleichtern die Definition und Verwendung eines Modells für eine Anwendung:
type alias Model =
{ count : Int
, lastEditMade : Time
}
Bei der Verwendung von type alias Typ mit dem von Ihnen angegebenen Namen buchstäblich Aliase verwendet. Die Verwendung von Model oben ist genau das gleiche wie mit { count : Int, lastEditMade : Time } . Hier ein Beispiel, das zeigt, wie sich Aliasnamen nicht von den zugrunde liegenden Typen unterscheiden:
type alias Bugatti = Int
type alias Fugazi = Int
unstoppableForceImmovableObject : Bugatti -> Fugazi -> Int
unstoppableForceImmovableObject bug fug =
bug + fug
> unstoppableForceImmovableObject 09 87
96 : Int
Ein Typalias für einen Datensatztyp definiert eine Konstruktorfunktion mit einem Argument für jedes Feld in Deklarationsreihenfolge.
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
Jeder Datensatztyp-Alias hat auch für einen kompatiblen Typ eine eigene Feldreihenfolge.
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
Verbesserung der Typsicherheit durch neue Typen
Aliasing-Typen reduzieren die Boilerplate und verbessern die Lesbarkeit. Sie sind jedoch nicht typsicherer als der Alias-Typ. Folgendes berücksichtigen:
type alias Email = String
type alias Name = String
someEmail = "[email protected]"
someName = "Benedict"
sendEmail : Email -> Cmd msg
sendEmail email = ...
Mit dem obigen Code können wir sendEmail someName schreiben, und es wird kompiliert, auch wenn es eigentlich nicht sollte, denn Namen und E-Mails sind beide String , aber sie sind völlig andere Dinge.
Wir können wirklich einen String von einem anderen String auf der Typebene unterscheiden, indem wir einen neuen Typ erstellen. Hier ein Beispiel, in dem Email als type und nicht als 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) = ...
Unsere isValid Funktion isValid , ob eine Zeichenfolge eine gültige E-Mail-Adresse ist. Die Funktion create prüft, ob ein bestimmter String eine gültige E-Mail ist, und sendet eine Maybe umschlossene Email um sicherzustellen, dass nur validierte Adressen zurückgegeben werden. Wir können die Validierungsprüfung umgehen, indem wir eine Email direkt EmailAddress "somestring" indem EmailAddress "somestring" , wenn unsere Moduldeklaration den EmailAddress Konstruktor nicht wie hier gezeigt EmailAddress
module Email exposing (Email, create, send)
In diesem EmailAddress kein anderes Modul Zugriff auf den EmailAddress Konstruktor, obwohl der Email Typ in Anmerkungen verwendet werden kann. Die einzige Möglichkeit, eine neue Email außerhalb dieses Moduls zu create ist die Verwendung der von ihr bereitgestellten create Diese Funktion stellt sicher, dass nur gültige E-Mail-Adressen zurückgegeben werden. Daher leitet diese API den Benutzer über den Typ "Sicherheit" automatisch in den richtigen Pfad zurück: send funktioniert nur mit von create Werten, die eine Gültigkeitsprüfung durchführen und die Verarbeitung ungültiger E-Mails erzwingen, da eine Maybe Email .
Wenn Sie den Email Konstruktor exportieren Email , können Sie schreiben
module Email exposing (Email(EmailAddress), create, send)
Nun kann jede Datei, die Email importiert, auch ihren Konstruktor importieren. In diesem Fall können Benutzer die Validierung umgehen und ungültige E-Mails send Sie erstellen jedoch nicht immer eine solche API, sodass das Exportieren von Konstruktoren hilfreich sein kann. Bei einem Typ mit mehreren Konstruktoren möchten Sie möglicherweise auch nur einige davon exportieren.
Typen konstruieren
Die Schlüsselwortkombination des type alias gibt einen neuen Namen für einen Typ an, das Schlüsselwort type isoliert jedoch einen neuen Typ. Untersuchen wir einen der grundlegendsten dieser Typen: Maybe
type Maybe a
= Just a
| Nothing
Das erste , was zu beachten ist , dass der Maybe Typ mit einer deklarieren Variable vom Typ von a . Die zweite Bemerkung ist das Pipe-Zeichen | , was "oder" bedeutet. Mit anderen Worten, etwas vom Typ Maybe a ist entweder Just a oder Nothing .
Wenn Sie den obigen Code zu schreiben, Just und Nothing kommen in Umfang als wertBauer, und Maybe kommt in Umfang als Typ-Konstruktor. Dies sind ihre Unterschriften:
Just : a -> Maybe a
Nothing : Maybe a
Maybe : a -> Maybe a -- this can only be used in type signatures
Aufgrund der Art Variable a kann jede Art „ nach innen eingewickelt“ werden von der Maybe geben. Also, Maybe Int , Maybe (List String) oder Maybe (Maybe (List Html)) alle gültigen Typen. Wenn jeder Destrukturierung type mit einem Wert case Ausdruck, müssen Sie für jede mögliche Instanziierung dieser Art erklären. Im Falle eines Werts vom Typ Maybe a müssen Sie sowohl den Fall Just a Fall als auch den Fall Nothing berücksichtigen:
thing : Maybe Int
thing =
Just 3
blah : Int
blah =
case thing of
Just n ->
n
Nothing ->
42
-- blah = 3
Versuchen Sie, den obigen Code ohne die Nothing Klausel im case Ausdruck zu schreiben: Er wird nicht kompiliert. Dies macht den Typ-Konstruktor von Maybe einem hervorragenden Muster zum Ausdrücken von Werten, die möglicherweise nicht vorhanden sind, da Sie dazu gezwungen werden, mit der Logik umzugehen, wenn der Wert Nothing .
Der Niemals-Typ
Der Typ " Never " kann nicht erstellt werden (das Basics Modul hat seinen Wertkonstruktor nicht exportiert und Ihnen keine andere Funktion gegeben, die auch " Never zurückgibt). Es gibt keinen Wert never : Never oder eine Funktion createNever : ?? -> Never .
Das hat seine Vorteile: Sie können in einem Typensystem eine Möglichkeit kodieren, die nicht möglich ist. Dies kann in Typen wie Task Never Int die garantieren, dass es mit einem Int gelingt. oder Program Never , das beim Initialisieren des Elm-Codes aus JavaScript keine Parameter übernimmt.
Spezielle Typvariablen
Elm definiert die folgenden speziellen Typvariablen, die für den Compiler eine bestimmte Bedeutung haben:
comparable: Bestehend ausInt,Float,Char,Stringund Tupeln davon. Dies ermöglicht die Verwendung der Operatoren<und>.Beispiel: Sie können eine Funktion definieren, um die kleinsten und größten Elemente in einer Liste (
extent) zu finden. Sie denken, welche Art von Signatur Sie schreiben. Auf der einen Seite könnten SieextentInt : List Int -> Maybe (Int, Int)schreibenextentInt : List Int -> Maybe (Int, Int)undextentChar : List Char -> Maybe (Char, Char)und ein weiteres fürFloatundString. Die Umsetzung dieser wäre die gleiche: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) xsSie könnten versucht sein, einfach einen Bereich zu schreiben
extent : List a -> Maybe (a, a), aber der Compiler lässt Sie das nicht, da die Funktionenminundmaxfür diese Typen nicht definiert sind (Achtung: Dies sind nur einfache Wrapper um den oben genannten<Operator). Sie können dieses Problem lösen, indem Sie denextent : List comparable -> Maybe (comparable, comparable)definierenextent : List comparable -> Maybe (comparable, comparable). Dadurch kann Ihre Lösung polymorph sein , was bedeutet, dass sie für mehrere Typen geeignet ist.number: Bestehend ausIntundFloat. Ermöglicht die Verwendung von arithmetischen Operatoren mit Ausnahme der Division. Sie können dann beispielsweise diesum : List number -> numberdefinierensum : List number -> numberund für Ints und Floats funktionieren lassen.appendable:appendableausString,List. Erlaubt die Verwendung des++Operators.compappend: Dies erscheint manchmal, ist jedoch ein Implementierungsdetail des Compilers. Derzeit kann dies nicht in Ihren eigenen Programmen verwendet werden, wird aber manchmal erwähnt.
Beachten Sie, dass in einer Typanmerkung wie folgt: number -> number -> number alle auf denselben Typ beziehen, so dass das Übergeben von Int -> Float -> Int ein Int -> Float -> Int wäre. Sie können dieses Problem lösen, indem Sie dem Typvariablennamen ein Suffix hinzufügen: number -> number' -> number'' würde dann gut kompilieren.
Es gibt keinen offiziellen Namen dafür, sie werden manchmal genannt:
- Spezielle Typvariablen
- Typklassentypartige Typvariablen
- Pseudo-Typ-Klassen
Dies liegt daran, dass sie wie die Typenklassen von Haskell arbeiten, ohne dass der Benutzer diese definieren kann.