W Elm nie mogę dowiedzieć się, kiedy type
jest 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?
Odpowiedzi:
Jak o tym myślę:
type
służy do definiowania nowych typów związków:
type Thing = Something | SomethingElse
Przed tą definicją Something
i SomethingElse
nic 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, Location
ponieważ 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 thing
jest a String
i aliasedStringIdentity
przyjmuje 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
{ 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
)
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 type
rozwią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 type
jako 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ć.
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.
type alias
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)
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.
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
.
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.
Task
w 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.
type
Definiowanie oznaczonego typu unii
Jest to najczęstszy przypadek użycia, dobrym przykładem jest Maybe
typ 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 Just
i Nothing
znaczniki nie tylko służyć jako funkcji konstruktora, służą również jako destruktorów lub wzorców w case
wypowiedzi. 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.
Ukrywanie szczegółów implementacji
Jak wskazano powyżej, celowym wyborem jest, aby funkcje konstruktorów Maybe
był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 Dict
i 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 Data
module może utworzyć wartość Person i tylko ten kod może dopasować do niej wzorzec.
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:8000
aby 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
An alias
to po prostu krótsza nazwa dla innego typu, podobna class
w 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 view
wiązu:
-- VIEW
...
case appState of
Loading -> showSpinner
Loaded -> showSuccessData
Error error -> showError
...
Myślę, że znasz różnicę między type
atype alias
.
Ale dlaczego i jak korzystać z aplikacji type
i type alias
jest to ważne elm
, możesz polecić artykuł Josha Claytona