Elm Language
Tipi, tipo variabili e tipo costruttori
Ricerca…
Osservazioni
Per favore, gioca con questi concetti per padroneggiarli davvero! L' elm-repl (vedere l' introduzione al REPL ) è probabilmente un buon posto per giocare con il codice sopra. Puoi anche giocare con elm-repl online .
Tipi di dati comparabili
I tipi comparabili sono tipi primitivi che possono essere confrontati usando operatori di confronto dal modulo Basics , come: (<) , (>) , (<=) , (>=) , max , min , compare
I tipi comparabili in Elm sono Int , Float , Time , Char , String e tuple o liste di tipi comparabili.
Nella documentazione o nella definizione dei tipi vengono indicati come variabili di tipo speciale comparable , ad es. vedere la definizione del tipo per la funzione Basics.max :
max : comparable -> comparable -> comparable
Digita le firme
In Elm, i valori vengono dichiarati scrivendo un nome, un segno di uguale e quindi il valore effettivo:
someValue = 42
Le funzioni sono anche valori, con l'aggiunta di prendere un valore o valori come argomenti. Di solito sono scritti come segue:
double n = n * 2
Ogni valore in Elm ha un tipo. I tipi di valori sopra indicati verranno dedotti dal compilatore in base a come vengono utilizzati. Tuttavia, è consigliabile dichiarare sempre esplicitamente il tipo di qualsiasi valore di livello superiore e, per farlo, scrivere una firma del tipo come segue:
someValue : Int
someValue =
42
someOtherValue : Float
someOtherValue =
42
Come possiamo vedere, 42 può essere definita come un Int o Float . Questo ha un senso intuitivo, ma vedi Tipo Variabili per maggiori informazioni.
Le firme di tipo sono particolarmente preziose se usate con le funzioni. Ecco la funzione di raddoppio di prima:
double : Int -> Int
double n =
n * 2
Questa volta, la firma ha una -> , una freccia e pronunciamo la firma come "int to int" o "prende un numero intero e restituisce un numero intero". -> indica che dando un valore double come argomento a Int , double restituirà un Int . Quindi, prende un intero per un numero intero:
> double
<function> : Int -> Int
> double 3
6 : Int
Tipi di base
In elm-repl , digita un pezzo di codice per ottenere il suo valore e il tipo dedotto. Prova quanto segue per conoscere i vari tipi esistenti:
> 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 }
> ()
() : ()
{} è il tipo di Record vuoto e () è il tipo di Tupla vuoto. Quest'ultimo è spesso usato ai fini della valutazione pigra. Vedere l'esempio corrispondente in Funzioni e Applicazione parziale .
Nota come il number appare nonapitalizzato. Ciò indica che è una variabile di tipo , e inoltre il particolare number parole si riferisce a una variabile di tipo speciale che può essere sia Int sia Float (vedere le sezioni corrispondenti per ulteriori informazioni). I tipi sono sempre in maiuscolo, ad esempio Char , Float , List String , eccetera.
Digita le variabili
Le variabili di tipo sono nomi non specializzati nelle firme dei tipi. A differenza delle loro controparti in maiuscolo, come Int e String , non rappresentano un singolo tipo, ma piuttosto un qualsiasi tipo. Sono utilizzati per scrivere funzioni generiche che possono funzionare su qualsiasi tipo o tipo e sono particolarmente utili per scrivere operazioni su contenitori come List o Dict . La funzione List.reverse , ad esempio, ha la seguente firma:
reverse : List a -> List a
Il che significa che può funzionare su un elenco di qualsiasi valore di tipo , quindi List Int , List (List String) , entrambi quelli e tutti gli altri possono essere reversed lo stesso. Quindi, a è una variabile di tipo che può essere valida per qualsiasi tipo.
La funzione reverse avrebbe potuto utilizzare qualsiasi nome di variabile incapitalizzata nella sua firma del tipo, ad eccezione della manciata di nomi di variabili di tipo speciale , come il number (vedere l'esempio corrispondente su quello per ulteriori informazioni):
reverse : List lol -> List lol
reverse : List wakaFlaka -> List wakaFlaka
I nomi delle variabili di tipo diventano significativi solo quando ci sono variabili di tipo diverse all'interno di una singola firma, esemplificate dalla funzione map sugli elenchi:
map : (a -> b) -> List a -> List b
map prende qualche funzione da qualsiasi tipo a a qualsiasi tipo b , insieme ad una lista con elementi di qualche tipo a , e restituisce una lista di elementi di qualche tipo b , che ottiene applicando la funzione data ad ogni elemento della lista.
Facciamo la firma concreta per vedere meglio questo:
plusOne : Int -> Int
plusOne x =
x + 1
> List.map plusOne
<function> : List Int -> List Int
Come possiamo vedere, sia a = Int che b = Int in questo caso. Ma, se la map avesse una map tipo firma come map : (a -> a) -> List a -> List a , allora funzionerebbe solo su funzioni che operano su un singolo tipo e non saresti mai in grado di cambiare il tipo di una lista usando la funzione map . Ma poiché la firma del tipo di map ha più variabili di tipo diverso, a e b , possiamo usare la map per cambiare il tipo di una lista:
isOdd : Int -> Bool
isOdd x =
x % 2 /= 0
> List.map isOdd
<function> : List Int -> List Bool
In questo caso, a = Int e b = Bool . Quindi, per essere in grado di utilizzare funzioni che possono assumere e restituire tipi diversi , è necessario utilizzare variabili di tipo diverse.
Digita alias
A volte vogliamo dare a un tipo un nome più descrittivo. Supponiamo che la nostra app abbia un tipo di dati che rappresenta gli utenti:
{ name : String, age : Int, email : String }
E le nostre funzioni sugli utenti hanno le firme del tipo sulla falsariga di:
prettyPrintUser : { name : String, age : Int, email : String } -> String
Questo potrebbe diventare piuttosto ingombrante con un tipo di record più grande per un utente, quindi usiamo un alias di tipo per ridurre le dimensioni e dare un nome più significativo a quella struttura di dati:
type alias User =
{ name: String
, age : Int
, email : String
}
prettyPrintUser : User -> String
Gli alias di tipo rendono molto più pulito definire e utilizzare un modello per un'applicazione:
type alias Model =
{ count : Int
, lastEditMade : Time
}
Usando l' type alias letteralmente si type alias solo un tipo con il nome che gli si dà. L'uso del tipo di Model sopra è esattamente lo stesso di usare { count : Int, lastEditMade : Time } . Ecco un esempio che mostra come gli alias non sono diversi dai tipi sottostanti:
type alias Bugatti = Int
type alias Fugazi = Int
unstoppableForceImmovableObject : Bugatti -> Fugazi -> Int
unstoppableForceImmovableObject bug fug =
bug + fug
> unstoppableForceImmovableObject 09 87
96 : Int
Un alias di tipo per un tipo di record definisce una funzione di costruzione con un argomento per ogni campo nell'ordine di dichiarazione.
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
Ogni alias del tipo di record ha il proprio ordine di campo anche per un tipo compatibile.
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
Migliorare la sicurezza del tipo usando nuovi tipi
I tipi di aliasing riducono lo standard di stampa e migliorano la leggibilità, ma non è più sicuro dal punto di vista del tipo aliasato. Considera quanto segue:
type alias Email = String
type alias Name = String
someEmail = "[email protected]"
someName = "Benedict"
sendEmail : Email -> Cmd msg
sendEmail email = ...
Usando il codice sopra, possiamo scrivere sendEmail someName , e verrà compilato, anche se in realtà non dovrebbe, perché nonostante i nomi e le email siano entrambi String s, sono cose completamente diverse.
Possiamo veramente distinguere una String da un'altra String a livello di carattere creando un nuovo tipo . Ecco un esempio che riscrive l' Email come type piuttosto che come 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) = ...
La isValid funzione isValid fa qualcosa per determinare se una stringa è un indirizzo email valido. Il create funzione controlla se una data String è un indirizzo email valido, restituendo una Maybe -wrapped Email per garantire che torniamo solo indirizzi convalidati. Mentre possiamo aggirare il controllo di validazione costruendo Email - Email direttamente scrivendo EmailAddress "somestring" , se la nostra dichiarazione del modulo non espone il costruttore EmailAddress , come mostrato qui
module Email exposing (Email, create, send)
quindi nessun altro modulo avrà accesso al costruttore EmailAddress , sebbene possano comunque utilizzare il tipo di Email nelle annotazioni. L' unico modo per creare una nuova e Email al di fuori di questo modulo è utilizzando la funzione di create che fornisce, e tale funzione garantisce che restituirà solo indirizzi email validi in primo luogo. Quindi, questa API guida automaticamente l'utente lungo il percorso corretto tramite il suo tipo di sicurezza: send solo lavori con valori costruiti da create , che esegue una convalida e impone la gestione di e-mail non valide poiché restituisce un messaggio di Maybe Email .
Se desideri esportare la funzione di costruzione Email , puoi scrivere
module Email exposing (Email(EmailAddress), create, send)
Ora qualsiasi file che importa Email può anche importare il suo costruttore. In questo caso, fare ciò consentirebbe agli utenti di aggirare la convalida e send e-mail non valide, ma non si sta sempre costruendo un'API come questa, quindi i costruttori di esportazione possono essere utili. Con un tipo con diversi costruttori, potresti anche voler esportarne solo alcuni.
Costruire tipi
La combinazione di parole chiave type alias dà un nuovo nome per un tipo, ma la parola chiave type in isolamento dichiara un nuovo tipo. Esaminiamo uno dei più fondamentali di questi tipi: Maybe
type Maybe a
= Just a
| Nothing
La prima cosa da notare è che il tipo Maybe è dichiarato con una variabile di tipo di a . La seconda cosa da notare è il carattere pipe, | , che significa "o". In altre parole, qualcosa di tipo Maybe a o è Just a o Nothing .
Quando scrivi il codice di cui sopra, Just e Nothing entrano in campo come costruttori di valori e Maybe entra in ambito come costruttore di tipi . Queste sono le loro firme:
Just : a -> Maybe a
Nothing : Maybe a
Maybe : a -> Maybe a -- this can only be used in type signatures
A causa della variabile di tipo a , qualsiasi tipo può essere "incluso all'interno" del tipo Maybe . Quindi, Maybe Int , Maybe (List String) , o Maybe (Maybe (List Html)) , sono tutti tipi validi. Quando si destrutturano qualsiasi valore di type con un'espressione di case , è necessario tenere conto di ogni possibile istanziazione di quel tipo. Nel caso di un valore di tipo Maybe a , devi tenere conto sia del caso Just a caso Nothing :
thing : Maybe Int
thing =
Just 3
blah : Int
blah =
case thing of
Just n ->
n
Nothing ->
42
-- blah = 3
Prova a scrivere il codice sopra senza la clausola Nothing nell'espressione case : non verrà compilato. Questo è ciò che rende il costruttore di tipo Maybe un ottimo pattern per esprimere valori che potrebbero non esistere, poiché ti costringe a gestire la logica di quando il valore è Nothing .
Il tipo Never
Il tipo Never non può essere costruito (il modulo Basics non ha esportato il suo costruttore di valori e non ha fornito alcuna altra funzione che restituisca Never ). Non c'è never : Never un valore never : Never o una funzione createNever : ?? -> Never .
Questo ha i suoi vantaggi: puoi codificare in un sistema di tipi una possibilità che non può accadere. Questo può essere visto in tipi come Task Never Int che garantisce che avrà successo con un Int ; o Program Never che non prenderà alcun parametro durante l'inizializzazione del codice Elm da JavaScript.
Variabili di tipo speciale
Elm definisce le seguenti variabili di tipo speciali che hanno un significato particolare per il compilatore:
comparable: Compreso diInt,Float,Char,Stringe tuple degli stessi. Questo consente l'uso degli operatori<e>.Esempio: è possibile definire una funzione per trovare gli elementi più piccoli e più grandi in una lista (
extent). Pensi che tipo di firma scrivere. Da un lato, è possibile scrivereextentInt : List Int -> Maybe (Int, Int)eextentChar : List Char -> Maybe (Char, Char)e un altro perFloateString. L'implementazione di questi sarebbe la stessa: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) xsPotresti essere tentato di scrivere semplicemente
extent : List a -> Maybe (a, a), ma il compilatore non ti permetterà di farlo, perché le funzioniminemaxnon sono definite per questi tipi (NB: questi sono solo semplici wrapper attorno all'operatore<cui sopra). Puoi risolvere questo problema definendo l'extent : List comparable -> Maybe (comparable, comparable). Ciò consente alla soluzione di essere polimorfica , il che significa che funzionerà per più di un tipo.number: Compreso diInteFloat. Permette l'uso di operatori aritmetici tranne la divisione. È quindi possibile definire ad esempio lasum : List number -> numbere farlo funzionare sia per interi che per float.appendable: composto daString,List. Permette l'uso dell'operatore++.compappend: questo a volte appare, ma è un dettaglio di implementazione del compilatore. Attualmente questo non può essere usato nei tuoi programmi, ma a volte viene menzionato.
Notare che in un'annotazione di tipo come questa: number -> number -> number questi si riferiscono tutti allo stesso tipo, quindi passare in Int -> Float -> Int sarebbe un errore di tipo. Puoi risolvere questo problema aggiungendo un suffisso al nome della variabile di tipo: number -> number' -> number'' quindi compilerebbe bene.
Non esiste un nome ufficiale per questi, a volte vengono chiamati:
- Variabili di tipo speciale
- Variabili di tipo simili al tipo di prodotto
- Pseudo-typeclasses
Questo perché funzionano come le classi di tipi di Haskell, ma senza la possibilità per l'utente di definirli.