Recherche…


Remarques

S'il vous plaît jouez avec ces concepts vous-même pour vraiment les maîtriser! L' elm-repl (voir l' introduction du REPL ) est probablement un bon endroit pour jouer avec le code ci-dessus. Vous pouvez également jouer avec elm-repl ligne .

Types de données comparables

Les types comparables sont des types primitifs pouvant être comparés à l'aide d'opérateurs de comparaison du module Basics , tels que: (<) , (>) , (<=) , (>=) , max , min , compare

Les types comparables dans Elm sont Int , Float , Time , Char , String et des tuples ou des listes de types comparables.

Dans la documentation ou les définitions de type, elles sont appelées variables de type spécial comparable , par exemple. voir la définition de type pour la fonction Basics.max :

max : comparable -> comparable -> comparable

Signatures de type

Dans Elm, les valeurs sont déclarées en écrivant un nom, un signe égal, puis la valeur réelle:

someValue = 42

Les fonctions sont également des valeurs, en plus de prendre une valeur ou des valeurs en argument. Ils sont généralement écrits comme suit:

double n = n * 2

Chaque valeur dans Elm a un type. Les types de valeurs ci-dessus seront déduits par le compilateur en fonction de leur utilisation. Toutefois, il est recommandé de toujours déclarer explicitement le type de toute valeur de niveau supérieur et, pour ce faire, écrivez une signature de type comme suit:

someValue : Int
someValue = 
    42

someOtherValue : Float
someOtherValue =
    42

Comme on peut le voir, 42 peut être définie comme un Int ou un Float . Cela a un sens intuitif, mais consultez Variables de type pour plus d'informations.

Les signatures de type sont particulièrement utiles lorsqu'elles sont utilisées avec des fonctions. Voici la fonction de doublage d'avant:

double : Int -> Int
double n =
    n * 2

Cette fois, la signature a un -> , une flèche, et nous prononçons la signature comme "int to int", ou "prend un entier et renvoie un entier". -> indique qu'en double une valeur Int comme argument, double retournera un Int . Par conséquent, il faut un entier pour un entier:

> double
<function> : Int -> Int

> double 3
6 : Int

Types de base

Dans elm-repl , tapez un morceau de code pour obtenir sa valeur et son type inféré. Essayez ce qui suit pour en savoir plus sur les différents types existants:

> 42
42 : number

> 1.987
1.987 : Float

> 42 / 2
21 : Float

> 42 % 2
0 : Int

> 'e'
'e' : Char

> "e"
"e" : String

> "Hello Friend"
"Hello Friend" : String

> ['w', 'o', 'a', 'h']
['w', 'o', 'a', 'h'] : List Char

> ("hey", 42.42, ['n', 'o'])
("hey", 42.42, ['n', 'o']) : ( String, Float, List Char )

> (1, 2.1, 3, 4.3, 'c')
(1,2.1,3,4.3,'c') : ( number, Float, number', Float, Char )

> {}
{} : {}

> { hey = "Hi", someNumber = 43 }
{ hey = "Hi", someNumber = 43 } : { hey : String, someNumber : number }

> ()
() : ()

{} est le type d'enregistrement vide et () est le type vide de Tuple. Ce dernier est souvent utilisé à des fins d'évaluation paresseuse. Voir l'exemple correspondant dans Fonctions et application partielle .

Notez comment le number semble non capitalisé. Cela indique qu'il s'agit d'une variable de type , et de plus, le number mot particulier fait référence à une variable de type spécial qui peut être un Int ou un Float (voir les sections correspondantes pour plus d'informations). Les types, cependant, sont toujours en majuscules, tels que Char , Float , List String , et cetera.

Variables de type

Les variables de type sont des noms non capitalisés dans les signatures de type. Contrairement à leurs homologues en majuscules, tels que Int et String , ils ne représentent pas un type unique, mais plutôt un type quelconque. Ils sont utilisés pour écrire des fonctions génériques pouvant fonctionner sur tout type ou type et sont particulièrement utiles pour écrire des opérations sur des conteneurs tels que List ou Dict . La fonction List.reverse , par exemple, a la signature suivante:

reverse : List a -> List a

Ce qui signifie qu'il peut fonctionner sur une liste de n'importe quelle valeur de type , de sorte que List Int , List (List String) , à la fois ceux-là et tous les autres, peuvent être reversed . Par conséquent, a est une variable de type qui peut figurer dans n'importe quel type.

La fonction reverse aurait pu utiliser n'importe quel nom de variable non capitalisé dans sa signature de type, à l'exception de quelques noms de variables de type spécial , tels que number (voir l'exemple correspondant pour plus d'informations):

reverse : List lol -> List lol

reverse : List wakaFlaka -> List wakaFlaka

Les noms des variables de type ne sont significatifs que lorsqu'il existe différentes variables de type dans une même signature, illustrées par la fonction de map sur les listes:

map : (a -> b) -> List a -> List b

map prend une fonction de n'importe quel type a à n'importe quel type b , avec une liste d'éléments de type a , et renvoie une liste d'éléments de type b , obtenus en appliquant la fonction donnée à chaque élément de la liste.

Faisons la signature concrète pour mieux voir ceci:

plusOne : Int -> Int
plusOne x = 
    x + 1

> List.map plusOne
<function> : List Int -> List Int

Comme on peut le voir, à la fois a = Int et b = Int dans ce cas. Mais si map avait une signature de type comme map : (a -> a) -> List a -> List a , alors cela ne fonctionnerait que sur les fonctions qui fonctionnent sur un seul type, et vous ne pourriez jamais changer la type d'une liste en utilisant la fonction de map . Mais comme la signature de type de map a plusieurs variables de type différentes, a et b , nous pouvons utiliser map pour changer le type d'une liste:

isOdd : Int -> Bool
isOdd x =
    x % 2 /= 0

> List.map isOdd
<function> : List Int -> List Bool

Dans ce cas, a = Int et b = Bool . Par conséquent, pour pouvoir utiliser des fonctions pouvant prendre et renvoyer des types différents , vous devez utiliser des variables de type différentes.

Alias ​​de type

Parfois, nous voulons donner à un type un nom plus descriptif. Disons que notre application a un type de données représentant les utilisateurs:

{ name : String, age : Int, email : String }

Et nos fonctions sur les utilisateurs ont des signatures de type dans le sens de:

prettyPrintUser : { name : String, age : Int, email : String } -> String

Cela pourrait devenir assez compliqué avec un type d'enregistrement plus important pour un utilisateur, alors utilisons un alias de type pour réduire la taille et donner un nom plus significatif à cette structure de données:

type alias User =
    { name: String
    , age : Int
    , email : String
    }


prettyPrintUser : User -> String

Les alias de type simplifient la définition et l'utilisation d'un modèle pour une application:

type alias Model =
    { count : Int
    , lastEditMade : Time
    }

Utiliser l' type alias littéralement ne fait qu'aliaser un type avec le nom que vous lui donnez. L'utilisation du type de Model ci-dessus est identique à l'utilisation de { count : Int, lastEditMade : Time } . Voici un exemple montrant comment les alias ne sont pas différents des types sous-jacents:

type alias Bugatti = Int

type alias Fugazi = Int

unstoppableForceImmovableObject : Bugatti -> Fugazi -> Int
unstoppableForceImmovableObject bug fug =
    bug + fug

> unstoppableForceImmovableObject 09 87
96 : Int

Un alias de type pour un type d'enregistrement définit une fonction constructeur avec un argument pour chaque champ dans l'ordre de déclaration.

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

Point 3 7
{ x = 3, y = 7 } : Point

type alias Person = { last : String, middle : String, first : String }

Person "McNameface" "M" "Namey"
{ last = "McNameface", middle = "M", first = "Namey" } : Person

Chaque alias de type d'enregistrement a son propre ordre de champ, même pour un type compatible.

type alias Person = { last : String, middle : String, first : String }
type alias Person2 = { first : String, last : String, middle : String }

Person2 "Theodore" "Roosevelt" "-"
{ first = "Theodore", last = "Roosevelt", middle = "-" } : Person2

a = [ Person "Last" "Middle" "First", Person2 "First" "Last" "Middle" ]
[{ last = "Last", middle = "Middle", first = "First" },{ first = "First", last = "Last", middle = "Middle" }] : List Person2

Amélioration de la sécurité des types en utilisant de nouveaux types

Les types d'alias réduisent les problèmes et améliorent la lisibilité, mais ce n'est pas plus sûr que le type alias lui-même. Considérer ce qui suit:

type alias Email = String

type alias Name = String

someEmail = "[email protected]"

someName = "Benedict"

sendEmail : Email -> Cmd msg
sendEmail email = ...

En utilisant le code ci-dessus, nous pouvons écrire sendEmail someName , et il compilera, même si cela ne devrait vraiment pas être le cas, car malgré les noms et les emails étant tous deux des String , ils sont complètement différents.

Nous pouvons vraiment distinguer une String d'une autre String au niveau du type en créant un nouveau type . Voici un exemple qui réécrit Email comme un type plutôt qu'un type alias :

module Email exposing (Email, create, send)

type Email = EmailAddress String

isValid : String -> Bool
isValid email = 
  -- ...validation logic

create : String -> Maybe Email
create email =
    if isValid email then
        Just (EmailAddress email)
    else
        Nothing

send : Email -> Cmd msg
send (EmailAddress email) = ...

Notre fonction isValid fait quelque chose pour déterminer si une chaîne est une adresse électronique valide. La fonction create vérifie si une String donnée est un email valide, renvoyant un Email Maybe -wrapped pour s'assurer que nous ne renvoyons que des adresses validées. Bien que nous puissions contourner le contrôle de validation en construisant un Email directement en écrivant EmailAddress "somestring" , si notre déclaration de module n'expose pas le constructeur EmailAddress , comme indiqué ici

module Email exposing (Email, create, send)

alors aucun autre module n'aura accès au constructeur EmailAddress , bien qu'ils puissent toujours utiliser le type Email dans les annotations. La seule façon de créer un nouvel Email dehors de ce module consiste à utiliser la fonction create qu'elle fournit et cette fonction garantit qu'elle ne renverra que des adresses e-mail valides. Par conséquent, cette API guide automatiquement l'utilisateur sur le chemin correct via son type de sécurité: send ne fonctionne qu'avec les valeurs construites par create , qui effectue une validation et applique le traitement des emails non valides car il retourne un Maybe Email - Maybe Email .

Si vous souhaitez exporter le constructeur Email , vous pouvez écrire

module Email exposing (Email(EmailAddress), create, send)

Désormais, tout fichier importé Email peut également importer son constructeur. Dans ce cas, cela permettrait aux utilisateurs de contourner la validation et d' send des e-mails non valides, mais vous ne construisez pas toujours une API comme celle-ci, donc les constructeurs exportateurs peuvent être utiles. Avec un type qui a plusieurs constructeurs, vous pouvez également ne vouloir exporter que certains d'entre eux.

Construire des types

La combinaison de mots-clés type alias donne un nouveau nom à un type, mais le mot-clé type déclare un nouveau type. Examinons l'un des plus fondamentaux de ces types: Maybe - Maybe

type Maybe a
    = Just a
    | Nothing

La première chose à noter est que le type Maybe est déclaré avec une variable de a . La deuxième chose à noter est le caractère de pipe, | , qui signifie "ou". En d'autres termes, quelque chose de type Maybe a est soit Just a ou Nothing .

Lorsque vous écrivez le code ci-dessus, Just et Nothing entrent en ligne de compte en tant que constructeurs de valeurs , et Maybe en tant que constructeur de type . Ce sont leurs signatures:

Just : a -> Maybe a

Nothing : Maybe a

Maybe : a -> Maybe a -- this can only be used in type signatures

En raison de la variable de type a , tout type peut être "enveloppé" du type Maybe . Donc, Maybe Int , Maybe (List String) ou Maybe (Maybe (List Html)) sont tous des types valides. Lors de la déstructuration de toute valeur de type avec une expression de case , vous devez tenir compte de chaque instanciation possible de ce type. Dans le cas d'une valeur de type Maybe a , vous devez tenir compte à la fois de l'affaire Just a case et Nothing :

thing : Maybe Int
thing = 
    Just 3

blah : Int
blah =
    case thing of
        Just n -> 
            n

        Nothing ->
            42

-- blah = 3

Essayez d'écrire le code ci-dessus sans la clause Nothing dans l'expression de case : cela ne compilera pas. C'est ce qui fait du constructeur de type Maybe un excellent modèle pour exprimer des valeurs qui peuvent ne pas exister, car il vous oblige à gérer la logique de la valeur Nothing .

Le type jamais

Le type Never ne peut pas être construit (le module Basics n’a pas exporté son constructeur de valeur et ne vous a pas donné d’autre fonction qui renvoie Never non plus). Il n'y a pas de valeur never : Never ou une fonction createNever : ?? -> Never .

Cela a ses avantages: vous pouvez encoder dans un système de type une possibilité qui ne peut pas arriver. Cela peut être vu dans des types comme Task Never Int qui garantit qu’il réussira avec un Int ; ou Program Never qui ne prendra aucun paramètre lors de l'initialisation du code Elm à partir de JavaScript.

Variables de type spécial

Elm définit les variables de type spéciales suivantes qui ont une signification particulière pour le compilateur:

  • comparable : composé de Int , Float , Char , String et tuples. Cela permet l'utilisation des opérateurs < et > .

    Exemple: Vous pouvez définir une fonction pour trouver les éléments les plus petits et les plus grands d'une liste ( extent ). Vous pensez quel type de signature écrire. D'une part, vous pouvez écrire extentInt : List Int -> Maybe (Int, Int) et extentChar : List Char -> Maybe (Char, Char) et un autre pour Float et String . La mise en œuvre de ces serait la même:

    extentInt list =
      let
        helper x (minimum, maximum) = 
          ((min minimum x), (max maximum x))
      in 
        case list of 
          [] ->
            Nothing
          x :: xs ->
            Just <| List.foldr helper (x, x) xs
    

    Vous pourriez être tenté d'écrire simplement extent : List a -> Maybe (a, a) , mais le compilateur ne vous laissera pas faire, car les fonctions min et max ne sont pas définies pour ces types (NB: ce ne sont que de simples wrappers) autour de l'opérateur < mentionné ci-dessus). Vous pouvez résoudre ce problème en définissant l' extent : List comparable -> Maybe (comparable, comparable) . Cela permet à votre solution d'être polymorphe , ce qui signifie simplement qu'elle fonctionnera pour plusieurs types.

  • number : composé de Int et Float . Permet l'utilisation d'opérateurs arithmétiques sauf division. Vous pouvez ensuite définir par exemple la sum : List number -> number et le faire fonctionner à la fois pour les ints et les flottants.

  • appendable : composé de String , List . Permet l'utilisation de l'opérateur ++ .

  • compappend : Ceci apparaît parfois, mais est un détail d'implémentation du compilateur. Actuellement, cela ne peut pas être utilisé dans vos propres programmes, mais est parfois mentionné.

Notez que dans une annotation de type comme celle-ci: number -> number -> number ils se réfèrent tous au même type, donc passer Int -> Float -> Int serait une erreur de type. Vous pouvez résoudre ce problème en ajoutant un suffixe au type nom de la variable: number -> number' -> number'' compilerait alors bien.

Il n'y a pas de nom officiel pour ceux-ci, ils sont parfois appelés:

  • Variables de type spécial
  • Variables de type typeclass-like
  • Pseudo-typeclasses

C'est parce qu'ils fonctionnent comme les classes de type de Haskell, mais sans la possibilité pour l'utilisateur de les définir.



Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow