Dans Elm, je ne peux pas savoir quand type
est le mot clé approprié par rapport à type alias
. La documentation ne semble pas avoir d'explication à cela, et je ne peux pas en trouver une dans les notes de version. Est-ce documenté quelque part?
Comment j'y pense:
type
est utilisé pour définir de nouveaux types d'unions:
type Thing = Something | SomethingElse
Avant cette définition, Something
et SomethingElse
ne signifiaient rien. Maintenant, ils sont tous les deux de type Thing
, que nous venons de définir.
type alias
est utilisé pour donner un nom à un autre type qui existe déjà:
type alias Location = { lat:Int, long:Int }
{ lat = 5, long = 10 }
a le type { lat:Int, long:Int }
, qui était déjà un type valide. Mais maintenant, nous pouvons également dire qu'il a le type Location
car il s'agit d'un alias pour le même type.
Il convient de noter que ce qui suit compilera très bien et affichera "thing"
. Même si nous spécifions que thing
est un String
et aliasedStringIdentity
prend un AliasedString
, nous n'obtiendrons pas d'erreur indiquant qu'il existe une incompatibilité de type 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
La clé est le mot alias
. Au cours de la programmation, quand on veut grouper des choses qui vont ensemble, on le met dans un disque, comme dans le cas d'un point
{ x = 5, y = 4 }
ou un dossier étudiant.
{ name = "Billy Bob", grade = 10, classof = 1998 }
Maintenant, si vous aviez besoin de transmettre ces enregistrements, vous devez épeler le type entier, comme:
add : { x:Int, y:Int } -> { x:Int, y:Int } -> { x:Int, y:Int }
add a b =
{ a.x + b.x, a.y + b.y }
Si vous pouviez alias un point, la signature serait tellement plus facile à écrire!
type alias Point = { x:Int, y:Int }
add : Point -> Point -> Point
add a b =
{ a.x + b.x, a.y + b.y }
Un alias est donc un raccourci pour autre chose. Ici, c'est un raccourci pour un type d'enregistrement. Vous pouvez le considérer comme donnant un nom à un type d'enregistrement que vous utiliserez souvent. C'est pourquoi on l'appelle un alias - c'est un autre nom pour le type d'enregistrement nu représenté par { x:Int, y:Int }
Alors que type
résout un problème différent. Si vous venez de la POO, c'est le problème que vous résolvez avec l'héritage, la surcharge d'opérateur, etc. - parfois, vous voulez traiter les données comme une chose générique, et parfois vous voulez la traiter comme une chose spécifique.
Un lieu commun où cela se produit est lors de la transmission de messages - comme le système postal. Lorsque vous envoyez une lettre, vous souhaitez que le système postal traite tous les messages comme la même chose, vous n'avez donc à concevoir le système postal qu'une seule fois. Et en outre, le travail de routage du message doit être indépendant du message qu'il contient. Ce n'est que lorsque la lettre atteint sa destination que vous vous souciez de ce qu'est le message.
De la même manière, nous pourrions définir un type
comme une union de tous les différents types de messages qui pourraient se produire. Imaginons que nous mettons en place un système de messagerie entre les étudiants et leurs parents. Il n'y a donc que deux messages que les collégiens peuvent envoyer: "J'ai besoin d'argent pour la bière" et "J'ai besoin d'un slip".
type MessageHome = NeedBeerMoney | NeedUnderpants
Alors maintenant, lorsque nous concevons le système de routage, les types de nos fonctions peuvent simplement faire circuler MessageHome
, au lieu de se soucier de tous les différents types de messages qu'il pourrait être. Le système de routage s'en fiche. Il suffit de savoir que c'est un MessageHome
. Ce n'est que lorsque le message atteint sa destination, le domicile des parents, que vous devez comprendre de quoi il s'agit.
case message of
NeedBeerMoney ->
sayNo()
NeedUnderpants ->
sendUnderpants(3)
Si vous connaissez l'architecture Elm, la fonction de mise à jour est une déclaration de cas géante, car c'est la destination de l'endroit où le message est acheminé, et donc traité. Et nous utilisons des types d'union pour avoir un seul type à traiter lors de la transmission du message, mais nous pouvons ensuite utiliser une instruction case pour déterminer exactement de quel message il s'agit, afin que nous puissions le traiter.
Permettez-moi de compléter les réponses précédentes en me concentrant sur les cas d'utilisation et en fournissant un peu de contexte sur les fonctions et modules constructeurs.
type alias
Créer un alias et une fonction constructeur pour un enregistrement
C'est le cas d'utilisation le plus courant: vous pouvez définir un nom alternatif et une fonction constructeur pour un type particulier de format d'enregistrement.
type alias Person =
{ name : String
, age : Int
}
La définition de l'alias de type implique automatiquement la fonction constructeur suivante (pseudo-code):Person : String -> Int -> { name : String, age : Int }
Cela peut être utile, par exemple lorsque vous souhaitez écrire un décodeur Json.
personDecoder : Json.Decode.Decoder Person
personDecoder =
Json.Decode.map2 Person
(Json.Decode.field "name" Json.Decode.String)
(Json.Decode.field "age" Int)
Spécifiez les champs obligatoires
Ils l'appellent parfois "enregistrements extensibles", ce qui peut être trompeur. Cette syntaxe peut être utilisée pour spécifier que vous attendez un enregistrement avec des champs particuliers présents. Tel que:
type alias NamedThing x =
{ x
| name : String
}
showName : NamedThing x -> Html msg
showName thing =
Html.text thing.name
Ensuite, vous pouvez utiliser la fonction ci-dessus comme ceci (par exemple à votre avis):
let
joe = { name = "Joe", age = 34 }
in
showName joe
conférence de Richard Feldman sur ElmEurope 2017 peut fournir un aperçu supplémentaire du moment où ce style mérite d'être utilisé.
Renommer des trucs
Vous pourriez le faire, car les nouveaux noms pourraient fournir une signification supplémentaire plus tard dans votre code, comme dans cet exemple
type alias Id = String
type alias ElapsedTime = Time
type SessionStatus
= NotStarted
| Active Id ElapsedTime
| Finished Id
Peut-être un meilleur exemple de ce type d'utilisation dans le noyau est Time
.
Ré-exposer un type d'un module différent
Si vous écrivez un package (pas une application), vous devrez peut-être implémenter un type dans un module, peut-être dans un module interne (non exposé), mais vous souhaitez exposer le type à partir d'un autre (public ) module. Ou, vous pouvez également exposer votre type à partir de plusieurs modules.Task
dans le noyau et Http.Request dans Http sont des exemples pour le premier, tandis que Json.Encode.Value et Json.Decode.Value paire est un exemple de la dernière.
Vous ne pouvez le faire que si vous souhaitez autrement garder le type opaque: vous n'exposez pas les fonctions du constructeur. Pour plus de détails, voir les utilisations de type
ci-dessous.
Il convient de noter que dans les exemples ci-dessus, seul # 1 fournit une fonction constructeur. Si vous exposez votre alias de type dans # 1 comme module Data exposing (Person)
cela exposera le nom du type ainsi que la fonction constructeur.
type
Définissez un type d'union balisé
C'est le cas d'utilisation le plus courant, un bon exemple en est le type Maybe
dans le noya :
type Maybe a
= Just a
| Nothing
Lorsque vous définissez un type, vous définissez également ses fonctions constructeur. Dans le cas de Peut-être que ce sont (pseudo-code):
Just : a -> Maybe a
Nothing : Maybe a
Ce qui signifie que si vous déclarez cette valeur:
mayHaveANumber : Maybe Int
Vous pouvez le créer soit par
mayHaveANumber = Nothing
ou
mayHaveANumber = Just 5
Les balises Just
et Nothing
ne servent pas seulement de fonctions constructeurs, elles servent également de destructeurs ou de motifs dans une expression case
. Ce qui signifie qu'en utilisant ces modèles, vous pouvez voir à l'intérieur d'un Maybe
:
showValue : Maybe Int -> Html msg
showValue mayHaveANumber =
case mayHaveANumber of
Nothing ->
Html.text "N/A"
Just number ->
Html.text (toString number)
Vous pouvez le faire, car le module Maybe est défini comme
module Maybe exposing
( Maybe(Just,Nothing)
Cela pourrait aussi dire
module Maybe exposing
( Maybe(..)
Les deux sont équivalents dans ce cas, mais être explicite est considéré comme une vertu dans Elm, surtout lorsque vous écrivez un package.
Masquage des détails d'implémentation
Comme indiqué ci-dessus, c'est un choix délibéré que les fonctions constructeurs de Maybe
soient visibles pour les autres modules.
Il existe cependant d'autres cas où l'auteur décide de les cacher. n exemple de ceci dans le noyau est Dict
. En tant que consommateur du package, vous ne devriez pas pouvoir voir les détails d'implémentation de l'algorithme d'arbre rouge/noir derrière Dict
et jouer directement avec les nœuds. Masquer les fonctions du constructeur force le consommateur de votre module/package à créer uniquement des valeurs de votre type (puis à transformer ces valeurs) via les fonctions que vous exposez.
C'est la raison pour laquelle parfois des trucs comme ça apparaissent dans le code
type Person =
Person { name : String, age : Int }
Contrairement à la définition type alias
En haut de cet article, cette syntaxe crée un nouveau type "union" avec une seule fonction constructeur, mais cette fonction constructeur peut être masquée des autres modules/packages.
Si le type est exposé comme ceci:
module Data exposing (Person)
Seul le code dans le module Data
peut créer une valeur Person et seul ce code peut y correspondre.
La principale différence, selon moi, est de savoir si le vérificateur de type vous crie dessus si vous utilisez un type "synomique".
Créez le fichier suivant, placez-le quelque part et exécutez Elm-reactor
, ensuite aller à http://localhost:8000
pour voir la différence:
-- 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
Si vous ne commentez pas 2.
et commentaire 1.
tu verras:
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
Un alias
est juste un nom plus court pour un autre type, similaire class
dans la POO. Exp:
type alias Point =
{ x : Int
, y : Int
}
Un type
(sans alias) vous permettra de définir votre propre type, afin que vous puissiez définir des types comme Int
, String
, ... pour votre application. Par exemple, dans le cas commun, il peut utiliser pour la description d'un état d'application:
type AppState =
Loading --loading state
|Loaded --load successful
|Error String --Loading error
Ainsi, vous pouvez facilement le gérer dans view
Elm:
-- VIEW
...
case appState of
Loading -> showSpinner
Loaded -> showSuccessData
Error error -> showError
...
Je pense que vous connaissez la différence entre type
et type alias
.
Mais pourquoi et comment utiliser type
et type alias
est important avec l'application Elm
, vous pouvez vous référer à article de Josh Clayton