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ę z Int , Float , Char , String i 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) i extentChar : List Char -> Maybe (Char, Char) a drugą dla Float i String . 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) xs
    

    Możesz mieć ochotę po prostu napisać extent : List a -> Maybe (a, a) , ale kompilator nie pozwoli ci tego zrobić, ponieważ funkcje min i max nie 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ąc extent : 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ę z Int i Float . Pozwala na użycie operatorów arytmetycznych z wyjątkiem dzielenia. Następnie możesz zdefiniować na przykład sum : List number -> number i sprawić, by działał zarówno dla liczb całkowitych, jak i liczb zmiennoprzecinkowych.

  • appendable : składa się z ciągu String , 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.



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow