Elm Language
Typy, zmienne typów i konstruktory typów
Szukaj…
Uwagi
Zagraj sam z tymi koncepcjami, aby naprawdę je opanować! elm-repl (patrz Wprowadzenie do REPL ) jest prawdopodobnie dobrym miejscem do zabawy z powyższym kodem. Możesz także grać z elm-repl online .
Porównywalne typy danych
Porównywalne typy to prymitywne typy, które można porównać za pomocą operatorów porównania z modułu Podstawy , takich jak: (<) , (>) , (<=) , (>=) , max , min , compare
Porównywalnymi typami w Elm są Int , Float , Time , Char , String i krotki lub listy porównywalnych typów.
W dokumentacji lub definicjach typów są one określane jako specjalna zmienna typu comparable , np. zobacz definicję typu dla funkcji Basics.max :
max : comparable -> comparable -> comparable
Wpisz podpisy
W Elm wartości deklaruje się, wpisując nazwę, znak równości, a następnie wartość rzeczywistą:
someValue = 42
Funkcje są również wartościami, z dodatkiem przyjmowania wartości lub wartości jako argumentów. Zazwyczaj są one napisane w następujący sposób:
double n = n * 2
Każda wartość w Elm ma swój typ. Typy powyższych wartości zostaną wyprowadzone przez kompilator w zależności od sposobu ich użycia. Ale najlepszą praktyką jest zawsze jawne deklarowanie typu dowolnej wartości najwyższego poziomu, a w tym celu należy napisać podpis typu w następujący sposób:
someValue : Int
someValue =
42
someOtherValue : Float
someOtherValue =
42
Jak można zauważyć, 42 może być zdefiniowana jako Int lub Float . Ma to intuicyjny sens, ale zobacz Zmienne typu, aby uzyskać więcej informacji.
Podpisy typów są szczególnie cenne, gdy są używane z funkcjami. Oto funkcja podwajania z wcześniej:
double : Int -> Int
double n =
n * 2
Tym razem podpis ma strzałkę -> , i wymawialibyśmy podpis jako „int do int”, lub „bierze liczbę całkowitą i zwraca liczbę całkowitą”. -> wskazuje, że dając double się Int wartość jako argument double zwróci Int . W związku z tym potrzeba liczby całkowitej na liczbę całkowitą:
> double
<function> : Int -> Int
> double 3
6 : Int
Podstawowe typy
W elm-repl wpisz fragment kodu, aby uzyskać jego wartość i wywnioskowany typ. Wypróbuj następujące informacje, aby dowiedzieć się o różnych istniejących typach:
> 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 }
> ()
() : ()
{} to pusty typ rekordu, a () to pusty typ krotki. Ten ostatni jest często używany do leniwej oceny. Zobacz odpowiedni przykład w funkcjach i częściowej aplikacji .
Zwróć uwagę na to, jak number wydaje się nie dokapitalizowana. Oznacza to, że jest to zmienna typu , a ponadto konkretny number słowa odnosi się do specjalnej zmiennej typu, która może być Int lub Float (więcej informacji w odpowiednich sekcjach). Typy są zawsze duże, takie jak Char , Float , List String i tak dalej.
Zmienne typu
Zmienne typu to niezapisane nazwy w podpisach typów. W przeciwieństwie do ich wielkich odpowiedników, takich jak Int i String , nie reprezentują one jednego typu, ale raczej dowolnego typu. Służą do pisania ogólnych funkcji, które mogą działać na dowolnym typie i typach, i są szczególnie przydatne do pisania operacji na kontenerach takich jak List lub Dict . Na List.reverse funkcja List.reverse ma następującą sygnaturę:
reverse : List a -> List a
Co oznacza, że może działać na liście dowolnej wartości typu , więc List Int , List (List String) , zarówno te, jak i inne, można reversed tak samo. Dlatego a jest zmienną typu, która może zastąpić dowolny typ.
Funkcja reverse mogła wykorzystać dowolną niekapitalizowaną nazwę zmiennej w podpisie typu, z wyjątkiem kilku nazw zmiennych specjalnych , takich jak number (więcej informacji na ten temat można znaleźć w odpowiednim przykładzie):
reverse : List lol -> List lol
reverse : List wakaFlaka -> List wakaFlaka
Nazwy zmiennych typu stają się znaczące tylko wtedy, gdy istnieją różne zmienne typu w obrębie jednej sygnatury, czego przykładem jest funkcja map na listach:
map : (a -> b) -> List a -> List b
map bierze jakąś funkcję z dowolnego typu a na dowolny typ b , wraz z listą z elementami jakiegoś typu a , i zwraca listę elementów jakiegoś typu b , którą otrzymuje poprzez zastosowanie danej funkcji do każdego elementu listy.
Uczyńmy podpis konkretnym, aby lepiej to zobaczyć:
plusOne : Int -> Int
plusOne x =
x + 1
> List.map plusOne
<function> : List Int -> List Int
Jak widzimy, w tym przypadku zarówno a = Int i b = Int . Ale jeśli map ma podpis typu podobny do map : (a -> a) -> List a -> List a , to działałoby tylko na funkcjach, które działają na jednym typie, i nigdy nie byłbyś w stanie zmienić typ listy za pomocą funkcji map . Ponieważ jednak podpis typu map zawiera wiele różnych typów zmiennych, a i b , możemy użyć map aby zmienić typ listy:
isOdd : Int -> Bool
isOdd x =
x % 2 /= 0
> List.map isOdd
<function> : List Int -> List Bool
W tym przypadku a = Int i b = Bool . Dlatego, aby móc korzystać z funkcji, które mogą przyjmować i zwracać różne typy, należy użyć zmiennych różnych typów.
Wpisz aliasy
Czasami chcemy nadać typowi bardziej opisową nazwę. Załóżmy, że nasza aplikacja ma typ danych reprezentujących użytkowników:
{ name : String, age : Int, email : String }
Nasze funkcje na użytkownikach mają podpisy typu, takie jak:
prettyPrintUser : { name : String, age : Int, email : String } -> String
Może to stać się niewygodne dla większego typu rekordu dla użytkownika, więc użyjmy aliasu typu, aby zmniejszyć rozmiar i nadać bardziej znaczącą nazwę tej strukturze danych:
type alias User =
{ name: String
, age : Int
, email : String
}
prettyPrintUser : User -> String
Aliasy typów znacznie ułatwiają definiowanie i stosowanie modelu dla aplikacji:
type alias Model =
{ count : Int
, lastEditMade : Time
}
Używanie type alias dosłownie type alias typu o nazwie, którą mu nadasz. Użycie powyższego typu Model jest dokładnie takie samo, jak użycie { count : Int, lastEditMade : Time } . Oto przykład pokazujący, jak aliasy nie różnią się od typów podstawowych:
type alias Bugatti = Int
type alias Fugazi = Int
unstoppableForceImmovableObject : Bugatti -> Fugazi -> Int
unstoppableForceImmovableObject bug fug =
bug + fug
> unstoppableForceImmovableObject 09 87
96 : Int
Alias typu dla typu rekordu definiuje funkcję konstruktora z jednym argumentem dla każdego pola w kolejności deklaracji.
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
Każdy alias typu rekordu ma własną kolejność pól, nawet dla zgodnego typu.
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
Poprawa bezpieczeństwa typu za pomocą nowych typów
Typy aliasingu zmniejszają płytę kotłową i zwiększają czytelność, ale nie są bardziej bezpieczne dla typu niż sam typ aliasu. Rozważ następujące:
type alias Email = String
type alias Name = String
someEmail = "[email protected]"
someName = "Benedict"
sendEmail : Email -> Cmd msg
sendEmail email = ...
Stosując powyższy kod, możemy napisać sendEmail someName , a będzie to skompilować, choć tak naprawdę nie powinno, ponieważ pomimo nazwy i e-maili zarówno jako String s, są zupełnie różne rzeczy.
Możemy naprawdę odróżnić jeden String od drugiego String na poziomie typu, tworząc nowy typ . Oto przykład, który przepisuje Email jako type a nie 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) = ...
Nasza funkcja isValid robi coś, aby ustalić, czy łańcuch jest prawidłowym adresem e-mail. Funkcja create sprawdza, czy dany String jest prawidłowym adresem Email -mail, zwracając wiadomość e-mail, która Maybe zawinięta, aby upewnić się, że zwracamy tylko zweryfikowane adresy. Chociaż możemy ominąć sprawdzanie poprawności przez konstruowania Email bezpośrednio pisząc EmailAddress "somestring" , jeśli nasza deklaracja moduł nie narażać EmailAddress konstruktora, jak pokazano tutaj
module Email exposing (Email, create, send)
wtedy żaden inny moduł nie będzie miał dostępu do konstruktora EmailAddress , chociaż nadal mogą używać typu Email w adnotacjach. Jedynym sposobem na zbudowanie nowej wiadomości Email poza tym modułem jest użycie funkcji create którą udostępnia, i ta funkcja zapewnia, że zwróci tylko prawidłowe adresy e-mail. Dlatego ten interfejs API automatycznie prowadzi użytkownika właściwą ścieżką poprzez bezpieczeństwo typu: send działa tylko z wartościami skonstruowanymi przez funkcję create , która wykonuje sprawdzanie poprawności i wymusza obsługę nieprawidłowych wiadomości e-mail, ponieważ zwraca wiadomość Maybe Email .
Jeśli chcesz wyeksportować konstruktora Email , możesz napisać
module Email exposing (Email(EmailAddress), create, send)
Teraz każdy plik, który importuje wiadomość Email może również importować swojego konstruktora. W takim przypadku pozwoliłoby to użytkownikom ominąć walidację i send nieprawidłowe e-maile, ale nie zawsze budujesz taki interfejs API, więc eksportowanie konstruktorów może być przydatne. W przypadku typu, który ma kilka konstruktorów, możesz chcieć wyeksportować tylko niektóre z nich.
Konstruowanie typów
Kombinacja słów kluczowych type alias daje nową nazwę typu, ale słowo kluczowe type w izolacji deklaruje nowy typ. Przeanalizujmy jeden z najbardziej podstawowych z tych typów: Maybe
type Maybe a
= Just a
| Nothing
Pierwszą rzeczą, na którą należy zwrócić uwagę jest to, że typ Maybe jest zadeklarowany za pomocą zmiennej typu a . Drugą rzeczą do odnotowania jest znak potoku, | , co oznacza „lub”. Innymi słowy, coś w rodzaju Maybe a jest albo po Just a albo Nothing .
Kiedy piszesz powyższy kod, Just and Nothing wchodzi w zakres jako konstruktory wartości , a Maybe wchodzi w zakres jako konstruktor typów . Oto ich podpisy:
Just : a -> Maybe a
Nothing : Maybe a
Maybe : a -> Maybe a -- this can only be used in type signatures
Ze względu na zmienną typu a , każdy typ może być „zawinięty” w typ typu Maybe . Więc Maybe Int , Maybe (List String) lub Maybe (Maybe (List Html)) są poprawnymi typami. Podczas destrukcji dowolnej wartości type za pomocą wyrażenia case należy uwzględnić każdą możliwą instancję tego typu. W przypadku wartości typu Maybe a , musisz uwzględnić zarówno przypadek Just a case, jak i Nothing :
thing : Maybe Int
thing =
Just 3
blah : Int
blah =
case thing of
Just n ->
n
Nothing ->
42
-- blah = 3
Spróbuj napisać powyższy kod bez klauzuli Nothing w wyrażeniu case : nie skompiluje się. To sprawia, że konstruktor typów Maybe jest świetnym wzorcem do wyrażania wartości, które mogą nie istnieć, ponieważ zmusza cię do posługiwania się logiką, kiedy wartość jest Nothing .
Nigdy nie pisz
Never można skonstruować typu Never (moduł Basics nie wyeksportował konstruktora wartości i nie podał żadnej innej funkcji, która zwraca wartość Never ). never : Never nie ma żadnej wartości never : Never lub funkcja createNever : ?? -> Never .
Ma to swoje zalety: można zakodować w systemie typów możliwość, która nie może się zdarzyć. Można to zaobserwować w typach takich jak Task Never Int które gwarantuje, że odniesie sukces z Int ; lub Program Never , który nie pobierze żadnych parametrów podczas inicjowania kodu wiązu z JavaScript.
Specjalne zmienne typu
Wiąz definiuje następujące zmienne typu specjalnego, które mają szczególne znaczenie dla kompilatora:
comparable: składa się zInt,Float,Char,Stringi krotek. Pozwala to na użycie operatorów<i>.Przykład: Możesz zdefiniować funkcję znajdującą najmniejsze i największe elementy na liście (
extent). Myślisz, jaki typ podpisu napisać. Z jednej strony możesz napisaćextentInt : List Int -> Maybe (Int, Int)iextentChar : List Char -> Maybe (Char, Char)a drugą dlaFloatiString. Ich wdrożenie byłoby takie samo: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) xsMożesz mieć ochotę po prostu napisać
extent : List a -> Maybe (a, a), ale kompilator nie pozwoli ci tego zrobić, ponieważ funkcjeminimaxnie są zdefiniowane dla tych typów (Uwaga: są to tylko proste opakowania wokół<operatora wspomnianego powyżej). Możesz rozwiązać ten problem, określającextent : List comparable -> Maybe (comparable, comparable). Dzięki temu Twoje rozwiązanie może być polimorficzne , co oznacza po prostu, że będzie działać dla więcej niż jednego typu.number: składa się zIntiFloat. Pozwala na użycie operatorów arytmetycznych z wyjątkiem dzielenia. Następnie możesz zdefiniować na przykładsum : List number -> numberi sprawić, by działał zarówno dla liczb całkowitych, jak i liczb zmiennoprzecinkowych.appendable: składa się z ciąguString,List. Pozwala na użycie operatora++.compappend: czasami się pojawia, ale jest szczegółem implementacji kompilatora. Obecnie nie można tego użyć we własnych programach, ale czasami jest o tym wspominany.
Zauważ, że w adnotacji typu takiej jak ta: number -> number -> number wszystkie odnoszą się do tego samego typu, więc przekazanie Int -> Float -> Int byłoby błędem typu. Możesz rozwiązać ten problem, dodając przyrostek do nazwy zmiennej typu: number -> number' -> number'' skompiluje się dobrze.
Nie ma na nie oficjalnej nazwy, czasem się nazywa:
- Specjalne zmienne typu
- Zmienne typu podobne do klas
- Pseudo-typy
Wynika to z faktu, że działają one jak klasy klas Haskella, ale bez możliwości ich zdefiniowania przez użytkownika.