Différence dans Elm entre le type et l'alias de type?

93

Dans Elm, je ne peux pas savoir quand typeest le mot - clé approprié par rapport type alias. La documentation ne semble pas avoir d'explication à ce sujet, et je ne peux pas en trouver une dans les notes de publication. Est-ce documenté quelque part?

ehdv
la source

Réponses:

136

Comment j'y pense:

type est utilisé pour définir de nouveaux types d'union:

type Thing = Something | SomethingElse

Avant cette définition Somethinget SomethingElsene signifiait 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 un type { lat:Int, long:Int }, qui était déjà un type valide. Mais maintenant, nous pouvons également dire qu'il a un type Locationcar c'est un alias pour le même type.

Il est à noter que ce qui suit se compilera très bien et s'affichera "thing". Même si nous spécifions thingest a Stringet aliasedStringIdentityprend an AliasedString, nous n'obtiendrons pas d'erreur indiquant qu'il y a 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
robertjlooby
la source
Je ne suis pas sûr de votre point du dernier paragraphe. Essayez-vous de dire qu'ils sont toujours du même type, peu importe comment vous l'aliasez?
ZHANG Cheng
7
Oui, soulignant simplement que le compilateur considère que le type
aliasé
Ainsi, lorsque vous utilisez la {}syntaxe d'enregistrement, vous définissez un nouveau type?
2
{ lat:Int, long:Int }ne définit pas un nouveau type. C'est déjà un type valide. type alias Location = { lat:Int, long:Int }ne définit pas non plus un nouveau type, il donne simplement un autre nom (peut-être plus descriptif) à un type déjà valide. type Location = Geo { lat:Int, long:Int }définirait un nouveau type ( Location)
robertjlooby
1
Quand faut-il utiliser l'alias type contre type? Où est l'inconvénient de toujours utiliser le type?
Richard Haven
8

La clé est le mot alias. Au cours de la programmation, lorsque vous souhaitez regrouper des éléments qui vont ensemble, vous le mettez dans un enregistrement, 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 devrez é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 y penser comme donner 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 typeré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érateurs, etc. - parfois, vous voulez traiter les données comme une chose générique, et parfois vous voulez les traiter comme une chose spécifique.

Un lieu courant où cela se produit est lors de la transmission de messages - comme le système postal. Lorsque vous envoyez une lettre, vous voulez 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 d'acheminement du message doit être indépendant du message contenu à l'intérieur. Ce n'est que lorsque la lettre atteint sa destination que vous vous souciez de la nature du message.

De la même manière, nous pourrions définir a typecomme une union de tous les différents types de messages qui pourraient arriver. Disons 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 étudiants peuvent envoyer: «J'ai besoin de bière» et «J'ai besoin de sous-vêtements».

type MessageHome = NeedBeerMoney | NeedUnderpants

Alors maintenant, lorsque nous concevons le système de routage, les types de nos fonctions peuvent simplement passer MessageHome, au lieu de se soucier de tous les différents types de messages que cela pourrait être. Le système de routage s'en fiche. Il suffit de savoir que c'est un fichier MessageHome. Ce n'est que lorsque le message atteint sa destination, la maison du parent, 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 instruction de cas géante, car c'est la destination d'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'agissait, afin que nous puissions le traiter.

Wilhelm
la source
5

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 les modules du constructeur.



Usages de type alias

  1. 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 autre nom 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)


  2. Spécifiez les champs obligatoires
    Ils appellent parfois cela des «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 dans votre vue):

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

    La conférence de Richard Feldman sur ElmEurope 2017 peut fournir un aperçu supplémentaire du moment où ce style vaut la peine d'être utilisé.

  3. Renommer des éléments
    Vous pouvez 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

    Un meilleur exemple de ce type d'utilisation dans le noyau est peut-êtreTime .

  4. Réexposer un type à partir 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 de un autre module (public). Ou bien, vous souhaitez exposer votre type à partir de plusieurs modules.
    Taskdans core 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 typeci - dessous.

Il est à noter que dans les exemples ci-dessus, seul le # 1 fournit une fonction constructeur. Si vous exposez votre alias de type dans # 1 comme module Data exposing (Person)cela, vous exposez le nom du type ainsi que la fonction constructeur.



Usages de type

  1. Définir un type d'union balisé
    C'est le cas d'utilisation le plus courant, un bon exemple de celui-ci est le Maybetype dans le noyau :

    type Maybe a
        = Just a
        | Nothing

    Lorsque vous définissez un type, vous définissez également ses fonctions de constructeur. Dans le cas de Maybe, 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

    mayHaveANumber = Nothing

    ou

    mayHaveANumber = Just 5

    Les balises Justet Nothingne servent pas seulement de fonctions de constructeur, elles servent également de destructeurs ou de modèles dans une caseexpression. Ce qui signifie qu'en utilisant ces modèles, vous pouvez voir à l'intérieur d'unMaybe :

    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, en particulier lorsque vous écrivez un package.


  1. Masquer les détails de l'implémentation
    Comme indiqué ci-dessus, c'est un choix délibéré que les fonctions du constructeur Maybesont visibles pour les autres modules.

    Il y a cependant d'autres cas où l'auteur décide de les cacher. Un exemple de ceci dans le noyau estDict . En tant que consommateur du package, vous ne devriez pas être en mesure de voir les détails d'implémentation de l'algorithme d'arbre rouge / noir derrière Dictet de jouer directement avec les nœuds. Le masquage des fonctions constructeur oblige 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 celui-ci apparaissent dans le code

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

    Contrairement au type alias définition 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 du Datamodule peut créer une valeur Person et seul ce code peut correspondre au modèle.

Gabor
la source
1

La principale différence, comme je le vois, est de savoir si le vérificateur de type crie sur vous si vous utilisez le type "synomical".

Créez le fichier suivant, placez-le quelque part et exécutez elm-reactor, puis accédez à http://localhost:8000pour 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 décommentez 2.et commentez, 1.vous verrez:

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
la source
0

An aliasest juste un nom plus court pour un autre type, similaire classen POO. Exp:

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

Un type(sans alias) vous permettra de définir votre propre type, de sorte que vous pouvez définir des types tels que Int, String... app pour vous. Par exemple, dans le cas courant, il peut utiliser pour la description d'un état de l'application:

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

Ainsi, vous pouvez facilement le manipuler en vieworme:

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

...

Je pense que vous connaissez la différence entre typeet type alias.

Mais pourquoi et comment utiliser typeet type aliasest important avec l' elmapplication, vous pouvez vous référer à l' article de Josh Clayton

hien
la source