Różnica w Elm między typem a aliasem typu?

93

W Elm nie mogę dowiedzieć się, kiedy typejest odpowiednie słowo kluczowe, a kiedy type alias. Wydaje się, że dokumentacja nie zawiera wyjaśnienia tego, ani nie mogę znaleźć takiego w uwagach do wydania. Czy jest to gdzieś udokumentowane?

ehdv
źródło

Odpowiedzi:

136

Jak o tym myślę:

type służy do definiowania nowych typów związków:

type Thing = Something | SomethingElse

Przed tą definicją Somethingi SomethingElsenic nie znaczy. Teraz oba są typu Thing, który właśnie zdefiniowaliśmy.

type alias służy do nadania nazwy innemu typowi, który już istnieje:

type alias Location = { lat:Int, long:Int }

{ lat = 5, long = 10 }ma typ { lat:Int, long:Int }, który był już prawidłowym typem. Ale teraz możemy również powiedzieć, że ma typ, Locationponieważ jest to alias dla tego samego typu.

Warto zauważyć, że następujące elementy będą dobrze się skompilować i wyświetlić "thing". Nawet jeśli określimy, że thingjest a Stringi aliasedStringIdentityprzyjmuje AliasedString, nie otrzymamy błędu, że istnieje niezgodność typów między String/ AliasedString:

import Graphics.Element exposing (show)

type alias AliasedString = String

aliasedStringIdentity: AliasedString -> AliasedString
aliasedStringIdentity s = s

thing : String
thing = "thing"

main =
  show <| aliasedStringIdentity thing
robertjlooby
źródło
Nie jestem pewien co do punktu z ostatniego akapitu. Czy próbujesz powiedzieć, że są nadal tego samego typu, bez względu na to, jak to zrobisz?
ZHANG Cheng
8
Tak, tylko wskazując, że kompilator uważa typ z
aliasem
Więc kiedy używasz {}składni rekordu, definiujesz nowy typ?
3
{ lat:Int, long:Int }nie definiuje nowego typu. To już prawidłowy typ. type alias Location = { lat:Int, long:Int }również nie definiuje nowego typu, po prostu nadaje inną (być może bardziej opisową) nazwę już poprawnemu typowi. type Location = Geo { lat:Int, long:Int }zdefiniowałby nowy typ ( Location)
robertjlooby
1
Kiedy należy używać typu, a kiedy aliasu typu? Jakie są wady ciągłego używania typu?
Richard Haven
8

Kluczem jest słowo alias. W trakcie programowania, gdy chcesz pogrupować rzeczy, które do siebie należą, umieszczasz to w rekordzie, jak w przypadku punktu

{ x = 5, y = 4 }  

lub rekord ucznia.

{ name = "Billy Bob", grade = 10, classof = 1998 }

Teraz, gdybyś musiał przekazać te rekordy, musiałbyś przeliterować cały typ, na przykład:

add : { x:Int, y:Int } -> { x:Int, y:Int } -> { x:Int, y:Int }
add a b =
  { a.x + b.x, a.y + b.y }

Gdybyś mógł aliasować punkt, podpis byłby o wiele łatwiejszy do napisania!

type alias Point = { x:Int, y:Int }
add : Point -> Point -> Point
add a b =
  { a.x + b.x, a.y + b.y }

Tak więc alias jest skrótem do czegoś innego. Tutaj jest skrótem dla typu rekordu. Możesz o tym myśleć jako o nadaniu nazwy typowi rekordu, którego będziesz często używać. Dlatego nazywa się to aliasem - to inna nazwa typu nagiego rekordu, który jest reprezentowany przez{ x:Int, y:Int }

Natomiast typerozwiązuje inny problem. Jeśli pochodzisz z OOP, jest to problem, który rozwiązujesz poprzez dziedziczenie, przeciążenie operatorów itp. - czasami chcesz traktować dane jako rzecz ogólną, a czasami chcesz traktować je jako konkretną rzecz.

Powszechnym zjawiskiem jest przekazywanie wiadomości - na przykład system pocztowy. Kiedy wysyłasz list, chcesz, aby system pocztowy traktował wszystkie wiadomości jako to samo, więc musisz zaprojektować system pocztowy tylko raz. Poza tym zadanie kierowania wiadomości powinno być niezależne od wiadomości w niej zawartej. Dopiero gdy list dotrze do miejsca przeznaczenia, zależy Ci na treści wiadomości.

W ten sam sposób możemy zdefiniować a typejako połączenie wszystkich różnych typów komunikatów, które mogą się zdarzyć. Powiedzmy, że wdrażamy system przesyłania wiadomości między studentami a ich rodzicami. Są więc tylko dwie wiadomości, które mogą wysłać dzieciaki ze studiów: „Potrzebuję pieniędzy na piwo” i „Potrzebuję majtek”.

type MessageHome = NeedBeerMoney | NeedUnderpants

Więc teraz, kiedy projektujemy system routingu, typy dla naszych funkcji mogą po prostu przejść MessageHome, zamiast martwić się o różne typy komunikatów, które mogą to być. System routingu nie dba o to. Wystarczy wiedzieć, że to plik MessageHome. Dopiero gdy wiadomość dotrze do miejsca docelowego, czyli do domu rodzica, musisz dowiedzieć się, co to jest.

case message of
  NeedBeerMoney ->
    sayNo()
  NeedUnderpants ->
    sendUnderpants(3)

Jeśli znasz architekturę Elm, funkcja update jest gigantyczną instrukcją case, ponieważ jest to miejsce docelowe, do którego wiadomość jest kierowana, a zatem przetwarzana. I używamy typów unii, aby mieć jeden typ do obsługi podczas przekazywania wiadomości, ale następnie możemy użyć instrukcji case, aby dowiedzieć się, jaki dokładnie był komunikat, abyśmy mogli sobie z tym poradzić.

Wilhelm
źródło
5

Pozwólcie, że uzupełnię poprzednie odpowiedzi, skupiając się na przypadkach użycia i przedstawiając mały kontekst na temat funkcji i modułów konstruktora.



Zastosowania type alias

  1. Tworzenie aliasu i funkcji konstruktora dla rekordu
    To najczęstszy przypadek użycia: możesz zdefiniować alternatywną nazwę i funkcję konstruktora dla określonego rodzaju formatu rekordu.

    type alias Person =
        { name : String
        , age : Int
        }
    

    Definiowanie aliasu typu automatycznie implikuje następującą funkcję konstruktora (pseudokod):
    Person : String -> Int -> { name : String, age : Int }
    Może się to przydać, na przykład, gdy chcesz napisać dekoder Json.

    personDecoder : Json.Decode.Decoder Person
    personDecoder =
        Json.Decode.map2 Person
            (Json.Decode.field "name" Json.Decode.String)
            (Json.Decode.field "age" Int)
    


  2. Określ wymagane pola
    Czasami nazywają to „rekordami rozszerzalnymi”, co może wprowadzać w błąd. Tej składni można użyć do określenia, że ​​oczekuje się jakiegoś rekordu z obecnymi określonymi polami. Jak na przykład:

    type alias NamedThing x =
        { x
            | name : String
        }
    
    showName : NamedThing x -> Html msg
    showName thing =
        Html.text thing.name
    

    Następnie możesz użyć powyższej funkcji w ten sposób (na przykład w swoim widoku):

    let
        joe = { name = "Joe", age = 34 }
    in
        showName joe
    

    Wykład Richarda Feldmana na temat ElmEurope 2017 może dostarczyć dalszych informacji na temat tego, kiedy warto stosować ten styl.

  3. Zmiana nazwy rzeczy
    nazw Możesz to zrobić, ponieważ nowe nazwy mogą nadać dodatkowe znaczenie w dalszej części kodu, jak w tym przykładzie

    type alias Id = String
    
    type alias ElapsedTime = Time
    
    type SessionStatus
        = NotStarted
        | Active Id ElapsedTime
        | Finished Id
    

    Być może lepszym przykładem tego rodzaju użycia w rdzeniu jestTime .

  4. Ponowne ujawnianie typu z innego modułu
    Jeśli piszesz pakiet (nie aplikację), może być konieczne zaimplementowanie typu w jednym module, być może w module wewnętrznym (nie ujawnionym), ale chcesz ujawnić typ z inny (publiczny) moduł. Lub, alternatywnie, chcesz ujawnić swój typ z wielu modułów.
    Taskw rdzeniu i Http.Request w Http są przykładami dla pierwszego, podczas gdy para Json.Encode.Value i Json.Decode.Value jest przykładem późniejszej.

    Możesz to zrobić tylko wtedy, gdy chcesz zachować nieprzezroczysty typ: nie ujawniasz funkcji konstruktora. Aby uzyskać szczegółowe informacje, zobacz zastosowaniatype poniżej.

Warto zauważyć, że w powyższych przykładach tylko # 1 udostępnia funkcję konstruktora. Jeśli ujawnisz swój alias typu w # 1 w module Data exposing (Person)ten sposób, ujawni się nazwa typu, a także funkcja konstruktora.



Zastosowania type

  1. Definiowanie oznaczonego typu unii
    Jest to najczęstszy przypadek użycia, dobrym przykładem jest Maybetyp w rdzeniu :

    type Maybe a
        = Just a
        | Nothing
    

    Definiując typ, definiujesz również jego funkcje konstruktora. W przypadku Może są to (pseudokod):

    Just : a -> Maybe a
    
    Nothing : Maybe a
    

    Co oznacza, że ​​jeśli zadeklarujesz tę wartość:

    mayHaveANumber : Maybe Int
    

    Możesz go utworzyć za pomocą jednego z nich

    mayHaveANumber = Nothing
    

    lub

    mayHaveANumber = Just 5
    

    Te Justi Nothingznaczniki nie tylko służyć jako funkcji konstruktora, służą również jako destruktorów lub wzorców w casewypowiedzi. Co oznacza, że ​​korzystając z tych wzorów możesz zobaczyć wnętrze Maybe:

    showValue : Maybe Int -> Html msg
    showValue mayHaveANumber =
        case mayHaveANumber of
            Nothing ->
                Html.text "N/A"
    
            Just number ->
                Html.text (toString number)
    

    Możesz to zrobić, ponieważ moduł Może jest zdefiniowany jako

    module Maybe exposing 
        ( Maybe(Just,Nothing)
    

    Mógłby też powiedzieć

    module Maybe exposing 
        ( Maybe(..)
    

    W tym przypadku oba są równoważne, ale bycie wyraźnym jest uważane za zaletę w Elm, szczególnie podczas pisania paczki.


  1. Ukrywanie szczegółów implementacji
    Jak wskazano powyżej, celowym wyborem jest, aby funkcje konstruktorów Maybebyły widoczne dla innych modułów.

    Są jednak inne przypadki, kiedy autor postanawia je ukryć. Jednym z przykładów tego w rdzeniu jestDict . Jako użytkownik pakietu nie powinieneś być w stanie zobaczyć szczegółów implementacji algorytmu czerwonego / czarnego drzewa stojącego za Dicti bezpośrednio zadzierać z węzłami. Ukrycie funkcji konstruktora zmusza konsumenta modułu / pakietu do tworzenia tylko wartości Twojego typu (a następnie przekształcania tych wartości) za pośrednictwem funkcji, które ujawniasz.

    To jest powód, dla którego czasami takie rzeczy pojawiają się w kodzie

    type Person =
        Person { name : String, age : Int }
    

    w przeciwieństwie do type alias definicji na początku tego postu, ta składnia tworzy nowy typ „unii” z tylko jedną funkcją konstruktora, ale ta funkcja konstruktora może być ukryta przed innymi modułami / pakietami.

    Jeśli typ jest ujawniony w ten sposób:

    module Data exposing (Person)
    

    Tylko kod w Datamodule może utworzyć wartość Person i tylko ten kod może dopasować do niej wzorzec.

Gabor
źródło
1

Główną różnicą, jak to widzę, jest to, czy sprawdzanie typów będzie krzyczeć na ciebie, jeśli używasz typu „synomicznego”.

Utwórz następujący plik, umieść go gdzieś i uruchom elm-reactor, a następnie przejdź do, http://localhost:8000aby zobaczyć różnicę:

-- Boilerplate code

module Main exposing (main)

import Html exposing (..)

main =
  Html.beginnerProgram
    {
      model = identity,
      view = view,
      update = identity
    }

-- Our type system

type alias IntRecordAlias = {x : Int}
type IntRecordType =
  IntRecordType {x : Int}

inc : {x : Int} -> {x : Int}
inc r = {r | x = .x r + 1}

view model =
  let
    -- 1. This will work
    r : IntRecordAlias
    r = {x = 1}

    -- 2. However, this won't work
    -- r : IntRecordType
    -- r = IntRecordType {x = 1}
  in
    Html.text <| toString <| inc r

Jeśli odkomentujesz 2.i skomentujesz 1., zobaczysz:

The argument to function `inc` is causing a mismatch.

34|                              inc r
                                     ^
Function `inc` is expecting the argument to be:

    { x : Int }

But it is:

    IntRecordType
EugZol
źródło
0

An aliasto po prostu krótsza nazwa dla innego typu, podobna classw OOP. EXP:

type alias Point =
  { x : Int
  , y : Int
  }

type(Bez aliasu) pozwoli Ci określić własny typ, więc można zdefiniować typy jak Int, String... dla Ciebie aplikacja. Na przykład, w typowym przypadku, może służyć do opisu stanu aplikacji:

type AppState = 
  Loading          --loading state
  |Loaded          --load successful
  |Error String    --Loading error

Więc możesz łatwo sobie z tym poradzić w viewwiązu:

-- VIEW
...
case appState of
    Loading -> showSpinner
    Loaded -> showSuccessData
    Error error -> showError

...

Myślę, że znasz różnicę między typeatype alias .

Ale dlaczego i jak korzystać z aplikacji typei type aliasjest to ważne elm, możesz polecić artykuł Josha Claytona

hien
źródło