Respostas:
Como eu penso sobre isso:
type
é usado para definir novos tipos de união:
type Thing = Something | SomethingElse
Antes dessa definição Something
e SomethingElse
não significava nada. Agora, ambos são do tipo Thing
, que acabamos de definir.
type alias
é usado para dar um nome a algum outro tipo que já existe:
type alias Location = { lat:Int, long:Int }
{ lat = 5, long = 10 }
tem tipo { lat:Int, long:Int }
, que já era um tipo válido. Mas agora também podemos dizer que tem tipo Location
porque é um apelido para o mesmo tipo.
É importante notar que o seguinte irá compilar perfeitamente e exibir "thing"
. Mesmo que especificar thing
é um String
e aliasedStringIdentity
tem um AliasedString
, não obterá um erro que há uma incompatibilidade de tipo entre 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 }
não define um novo tipo. Esse já é um tipo válido. type alias Location = { lat:Int, long:Int }
também não define um novo tipo, apenas dá outro nome (talvez mais descritivo) a um tipo já válido. type Location = Geo { lat:Int, long:Int }
definiria um novo tipo ( Location
)
A chave é a palavra alias
. No decorrer da programação, quando você quer agrupar coisas que pertencem umas às outras, você coloca em um registro, como no caso de um ponto
{ x = 5, y = 4 }
ou um registro do aluno.
{ name = "Billy Bob", grade = 10, classof = 1998 }
Agora, se você precisar passar esses registros, terá que soletrar o tipo inteiro, como:
add : { x:Int, y:Int } -> { x:Int, y:Int } -> { x:Int, y:Int }
add a b =
{ a.x + b.x, a.y + b.y }
Se você pudesse criar um alias para um ponto, a assinatura seria muito mais fácil de escrever!
type alias Point = { x:Int, y:Int }
add : Point -> Point -> Point
add a b =
{ a.x + b.x, a.y + b.y }
Portanto, um alias é uma abreviação para outra coisa. Aqui, é uma abreviatura para um tipo de registro. Você pode pensar nisso como dar um nome a um tipo de registro que usará com frequência. É por isso que é chamado de alias - é outro nome para o tipo de registro simples que é representado por{ x:Int, y:Int }
Considerando que type
resolve um problema diferente. Se você está vindo de OOP, é o problema que você resolve com herança, sobrecarga de operador, etc. - às vezes, você deseja tratar os dados como algo genérico e às vezes deseja tratá-los como algo específico.
Um lugar comum onde isso acontece é ao passar mensagens - como o sistema postal. Ao enviar uma carta, você deseja que o sistema postal trate todas as mensagens como a mesma coisa; portanto, você só precisa projetar o sistema postal uma vez. Além disso, o trabalho de roteamento da mensagem deve ser independente da mensagem contida nela. É apenas quando a carta chega ao seu destino que você se preocupa com a mensagem.
Da mesma forma, podemos definir a type
como a união de todos os diferentes tipos de mensagens que podem acontecer. Digamos que estejamos implementando um sistema de mensagens entre estudantes universitários para seus pais. Portanto, há apenas duas mensagens que os universitários podem enviar: 'Preciso de dinheiro para cerveja' e 'Preciso de cuecas'.
type MessageHome = NeedBeerMoney | NeedUnderpants
Portanto, agora, quando projetamos o sistema de roteamento, os tipos de nossas funções podem apenas passar MessageHome
, em vez de nos preocuparmos com todos os diferentes tipos de mensagens que poderiam ser. O sistema de roteamento não se importa. Ele só precisa saber que é um MessageHome
. É apenas quando a mensagem chega ao seu destino, a casa dos pais, que você precisa descobrir o que é.
case message of
NeedBeerMoney ->
sayNo()
NeedUnderpants ->
sendUnderpants(3)
Se você conhece a arquitetura Elm, a função de atualização é uma declaração de caso gigante, porque esse é o destino de onde a mensagem é roteada e, portanto, processada. E usamos tipos de união para ter um único tipo com o qual lidar ao passar a mensagem, mas então podemos usar uma instrução case para descobrir exatamente que mensagem era, para que possamos lidar com isso.
Deixe-me complementar as respostas anteriores focalizando os casos de uso e fornecendo um pouco de contexto nas funções e módulos do construtor.
type alias
Criar um alias e uma função construtora para um registro
Este é o caso de uso mais comum: você pode definir um nome alternativo e uma função construtora para um tipo específico de formato de registro.
type alias Person =
{ name : String
, age : Int
}
Definir o alias do tipo implica automaticamente a seguinte função de construtor (pseudocódigo):
Person : String -> Int -> { name : String, age : Int }
Isso pode ser útil, por exemplo, quando você deseja escrever um decodificador Json.
personDecoder : Json.Decode.Decoder Person
personDecoder =
Json.Decode.map2 Person
(Json.Decode.field "name" Json.Decode.String)
(Json.Decode.field "age" Int)
Especifique os campos obrigatórios.
Às vezes, eles chamam de "registros extensíveis", o que pode ser enganoso. Esta sintaxe pode ser usada para especificar que você está esperando algum registro com campos específicos presentes. Tal como:
type alias NamedThing x =
{ x
| name : String
}
showName : NamedThing x -> Html msg
showName thing =
Html.text thing.name
Em seguida, você pode usar a função acima como esta (por exemplo, em sua visualização):
let
joe = { name = "Joe", age = 34 }
in
showName joe
A palestra de Richard Feldman no ElmEurope 2017 pode fornecer mais informações sobre quando vale a pena usar esse estilo.
Renomeando coisas
Você pode fazer isso, porque os novos nomes podem fornecer um significado extra posteriormente em seu código, como neste exemplo
type alias Id = String
type alias ElapsedTime = Time
type SessionStatus
= NotStarted
| Active Id ElapsedTime
| Finished Id
Talvez um exemplo melhor desse tipo de uso no núcleo sejaTime
.
Reexpondo um tipo de um módulo diferente
Se você estiver escrevendo um pacote (não um aplicativo), pode ser necessário implementar um tipo em um módulo, talvez em um módulo interno (não exposto), mas deseja expor o tipo de um módulo diferente (público). Ou, alternativamente, você deseja expor seu tipo de vários módulos.
Task
no núcleo e Http.Request em HTTP são exemplos para o primeiro, enquanto o Json.Encode.Value e Json.Decode.Value par é um exemplo do mais tarde.
Você só pode fazer isso quando, de outra forma, deseja manter o tipo opaco: você não expõe as funções do construtor. Para obter detalhes, consulte os usos type
abaixo.
É importante notar que nos exemplos acima apenas o # 1 fornece uma função de construtor. Se você expor seu alias de tipo em # 1 assim, module Data exposing (Person)
irá expor o nome do tipo, bem como a função do construtor.
type
Defina um tipo de união marcada
Este é o caso de uso mais comum, um bom exemplo disso é o Maybe
tipo em núcleo :
type Maybe a
= Just a
| Nothing
Ao definir um tipo, você também define suas funções de construtor. No caso de Maybe estes são (pseudocódigo):
Just : a -> Maybe a
Nothing : Maybe a
O que significa que se você declarar este valor:
mayHaveANumber : Maybe Int
Você pode criá-lo por qualquer
mayHaveANumber = Nothing
ou
mayHaveANumber = Just 5
As tags Just
e Nothing
não servem apenas como funções construtoras, mas também como destruidores ou padrões em uma case
expressão. O que significa que, usando esses padrões, você pode ver dentro de Maybe
:
showValue : Maybe Int -> Html msg
showValue mayHaveANumber =
case mayHaveANumber of
Nothing ->
Html.text "N/A"
Just number ->
Html.text (toString number)
Você pode fazer isso, porque o módulo Maybe é definido como
module Maybe exposing
( Maybe(Just,Nothing)
Também poderia dizer
module Maybe exposing
( Maybe(..)
Os dois são equivalentes neste caso, mas ser explícito é considerado uma virtude no Elm, especialmente quando você está escrevendo um pacote.
Ocultando detalhes de implementação
Como apontado acima, é uma escolha deliberada que as funções do construtor Maybe
sejam visíveis para outros módulos.
Há outros casos, porém, em que o autor decide ocultá-los. Um exemplo disso no núcleo éDict
. Como consumidor do pacote, você não deve ser capaz de ver os detalhes de implementação do algoritmo de árvore Vermelho / Preto por trás Dict
e mexer com os nós diretamente. Ocultar as funções do construtor força o consumidor do seu módulo / pacote a criar apenas valores do seu tipo (e então transformar esses valores) por meio das funções que você expõe.
Esta é a razão pela qual às vezes coisas assim aparecem no código
type Person =
Person { name : String, age : Int }
Ao contrário da type alias
definição no início deste post, esta sintaxe cria um novo tipo de "união" com apenas uma função de construtor, mas essa função de construtor pode ser oculta de outros módulos / pacotes.
Se o tipo for exposto assim:
module Data exposing (Person)
Apenas o código no Data
módulo pode criar um valor Person e apenas esse código pode corresponder a um padrão nele.
A principal diferença, a meu ver, é se o verificador de tipos gritará com você se você usar o tipo "sinômico".
Crie o seguinte arquivo, coloque-o em algum lugar e execute-o elm-reactor
, em seguida, vá para http://localhost:8000
para ver a diferença:
-- 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
Se você descomentar 2.
e comentar 1.
, verá:
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
Um alias
é apenas um nome mais curto para algum outro tipo, semelhante class
em OOP. Exp:
type alias Point =
{ x : Int
, y : Int
}
Um type
(sem alias) vai deixar você definir seu próprio tipo, para que possa definir tipos, como Int
, String
... para você aplicativo. Por exemplo, em casos comuns, pode ser usado para a descrição de um estado de aplicativo:
type AppState =
Loading --loading state
|Loaded --load successful
|Error String --Loading error
Assim, você pode manipulá-lo facilmente no view
olmo:
-- VIEW
...
case appState of
Loading -> showSpinner
Loaded -> showSuccessData
Error error -> showError
...
Acho que você sabe a diferença entre type
e type alias
.
Mas por que e como usar type
e type alias
é importante com o elm
aplicativo, vocês podem consultar o artigo de Josh Clayton