Elm Language
Tipos, variables de tipo y constructores de tipo
Buscar..
Observaciones
¡Por favor juega con estos conceptos para dominarlos realmente! elm-repl (ver la Introducción al REPL ) es probablemente un buen lugar para jugar con el código anterior. También puedes jugar con elm-repl online .
Tipos de datos comparables
Los tipos comparables son tipos primitivos que pueden compararse utilizando operadores de comparación del módulo de Conceptos básicos , como: (<) , (>) , (<=) , (>=) , max , min , compare
Los tipos comparables en Elm son Int , Float , Time , Char , String y tuplas o listas de tipos comparables.
En la documentación o en las definiciones de tipo se les conoce como una variable de tipo especial comparable , por ejemplo. ver definición de tipo para la función Basics.max :
max : comparable -> comparable -> comparable
Tipo de firmas
En Elm, los valores se declaran escribiendo un nombre, un signo igual y luego el valor real:
someValue = 42
Las funciones también son valores, con la adición de tomar un valor o valores como argumentos. Por lo general se escriben de la siguiente manera:
double n = n * 2
Cada valor en Elm tiene un tipo. Los tipos de los valores anteriores se infiere por el compilador dependiendo de cómo se utilizan. Pero es una buena práctica siempre declarar explícitamente el tipo de cualquier valor de nivel superior, y para hacerlo, escriba una firma de tipo de la siguiente manera:
someValue : Int
someValue =
42
someOtherValue : Float
someOtherValue =
42
Como podemos ver, 42 se puede definir como o bien un Int o un Float . Esto tiene sentido intuitivo, pero consulte Variables de tipo para obtener más información.
Las firmas de tipos son particularmente valiosas cuando se usan con funciones. Aquí está la función de duplicación de antes:
double : Int -> Int
double n =
n * 2
Esta vez, la firma tiene una -> , una flecha, y la pronunciaríamos como "int to int", o "toma un entero y devuelve un entero". -> indica que al dar un valor Int double como argumento, double devolverá un Int . Por lo tanto, toma un entero a un entero:
> double
<function> : Int -> Int
> double 3
6 : Int
Tipos basicos
En elm-repl , escriba un fragmento de código para obtener su valor y tipo inferido. Intente lo siguiente para aprender sobre los diversos tipos que existen:
> 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 }
> ()
() : ()
{} es el tipo de registro vacío, y () es el tipo de tupla vacío. Este último se utiliza a menudo para fines de evaluación perezosa. Vea el ejemplo correspondiente en Funciones y Aplicación Parcial .
Observe cómo el number aparece sin capitalizar. Esto indica que es una Variable de tipo y, además, el number palabra en particular se refiere a una Variable de tipo especial que puede ser Int o Float (consulte las secciones correspondientes para obtener más información). Sin embargo, los tipos siempre están en mayúsculas, como Char , Float , List String , etcétera.
Variables de tipo
Las variables de tipo son nombres sin capitalizar en las firmas de tipo. A diferencia de sus homólogos en mayúsculas, como Int y String , no representan un solo tipo, sino cualquier tipo. Se utilizan para escribir funciones genéricas que pueden operar en cualquier tipo o tipo, y son particularmente útiles para escribir operaciones en contenedores como List o Dict . La función List.reverse , por ejemplo, tiene la siguiente firma:
reverse : List a -> List a
Lo que significa que puede funcionar en una lista de cualquier valor de tipo , por lo que List Int , List (List String) , ambos y cualquier otro puede reversed la misma manera. Por lo tanto, a es una variable de tipo que puede representar cualquier tipo.
La función reverse podría haber usado cualquier nombre de variable sin capitalizar en su firma de tipo, excepto por un puñado de nombres de variables de tipo especial , como el number (consulte el ejemplo correspondiente en este enlace para obtener más información):
reverse : List lol -> List lol
reverse : List wakaFlaka -> List wakaFlaka
Los nombres de las variables de tipo se vuelven significativos solo cuando hay diferentes variables de tipo dentro de una única firma, ejemplificada por la función de map en las listas:
map : (a -> b) -> List a -> List b
map toma alguna función de cualquier tipo a a cualquier tipo b , junto con una lista con los elementos de algún tipo a , y devuelve una lista de elementos de algún tipo b , que se pone mediante la aplicación de la función dada a cada elemento de la lista.
Hagamos la firma concreta para ver mejor esto:
plusOne : Int -> Int
plusOne x =
x + 1
> List.map plusOne
<function> : List Int -> List Int
Como podemos ver, tanto a = Int como b = Int en este caso. Pero, si el map tuviera una firma de tipo como map : (a -> a) -> List a -> List a , entonces solo funcionaría en funciones que operan en un solo tipo, y nunca sería capaz de cambiar el tipo de una lista mediante la función de map . Pero dado que el tipo de firma del map tiene múltiples variables de tipo diferente, a y b , podemos usar map para cambiar el tipo de una lista:
isOdd : Int -> Bool
isOdd x =
x % 2 /= 0
> List.map isOdd
<function> : List Int -> List Bool
En este caso, a = Int y b = Bool . Por lo tanto, para poder usar funciones que pueden tomar y devolver diferentes tipos, debe usar diferentes variables de tipo.
Tipo de alias
A veces queremos darle a un tipo un nombre más descriptivo. Digamos que nuestra aplicación tiene un tipo de datos que representa a los usuarios:
{ name : String, age : Int, email : String }
Y nuestras funciones en los usuarios tienen firmas de tipo en la línea de:
prettyPrintUser : { name : String, age : Int, email : String } -> String
Esto podría volverse bastante difícil de manejar con un tipo de registro más grande para un usuario, así que usemos un alias de tipo para reducir el tamaño y darle un nombre más significativo a esa estructura de datos:
type alias User =
{ name: String
, age : Int
, email : String
}
prettyPrintUser : User -> String
Los alias de tipo hacen que sea mucho más limpio definir y usar un modelo para una aplicación:
type alias Model =
{ count : Int
, lastEditMade : Time
}
Usar el type alias literalmente solo alias un tipo con el nombre que le das. Usar el tipo de Model anterior es exactamente lo mismo que usar { count : Int, lastEditMade : Time } . Aquí hay un ejemplo que muestra cómo los alias no son diferentes a los tipos subyacentes:
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 tipo para un tipo de registro define una función constructora con un argumento para cada campo en el orden de declaración.
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
Cada alias de tipo de registro tiene su propio orden de campo incluso para un tipo 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
Mejora de la seguridad de tipos utilizando nuevos tipos
Los tipos de alias reducen la repetición y mejoran la legibilidad, pero no son más seguros de lo que son los tipos con alias. Considera lo siguiente:
type alias Email = String
type alias Name = String
someEmail = "[email protected]"
someName = "Benedict"
sendEmail : Email -> Cmd msg
sendEmail email = ...
Usando el código anterior, podemos escribir sendEmail someName , y se compilará, aunque realmente no debería, porque a pesar de que los nombres y los correos electrónicos son String , son cosas completamente diferentes.
Podemos distinguir realmente una String de otra String en el nivel de tipo creando un nuevo tipo . Aquí hay un ejemplo que reescribe el Email como un type lugar de 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) = ...
Nuestra función isValid hace algo para determinar si una cadena es una dirección de correo electrónico válida. El create función comprueba si un determinado String es un correo electrónico válido, devolviendo un Maybe -wrapped Email para asegurarse de que sólo volvemos direcciones validadas. Si bien podemos esquivar la verificación de validación construyendo un Email directamente escribiendo EmailAddress "somestring" , si nuestra declaración de módulo no expone el constructor EmailAddress , como se muestra aquí
module Email exposing (Email, create, send)
entonces ningún otro módulo tendrá acceso al constructor EmailAddress , aunque aún pueden usar el tipo de Email en las anotaciones. La única forma de crear un nuevo Email fuera de este módulo es mediante la función de create que proporciona, y esa función garantiza que solo devolverá direcciones de correo electrónico válidas en primer lugar. Por lo tanto, esta API guía automáticamente al usuario por el camino correcto a través de su seguridad de tipo: send solo funciona con los valores construidos por create , que realiza una validación, e impone el manejo de correos electrónicos no válidos, ya que devuelve un Maybe Email .
Si desea exportar el constructor de Email , puede escribir
module Email exposing (Email(EmailAddress), create, send)
Ahora cualquier archivo que importe Email también puede importar su constructor. En este caso, hacerlo permitiría a los usuarios eludir la validación y send correos electrónicos no válidos, pero no siempre está creando una API como esta, por lo que exportar constructores puede ser útil. Con un tipo que tiene varios constructores, también es posible que solo desee exportar algunos de ellos.
Construyendo tipos
La combinación de palabras clave de type alias da un nuevo nombre para un tipo, pero la palabra clave de type en aislamiento declara un nuevo tipo. Examinemos uno de los más fundamentales de estos tipos: Maybe
type Maybe a
= Just a
| Nothing
Lo primero que se debe tener en cuenta es que el tipo Maybe se declara con una variable de tipo de a . La segunda cosa a tener en cuenta es el carácter de la tubería, | , que significa "o". En otras palabras, algo de tipo Maybe a sea Just a or Nothing .
Cuando escribe el código anterior, Just and Nothing entra en el alcance como constructores de valor , y Maybe entra en el alcance como un constructor de tipo . Estas son sus firmas:
Just : a -> Maybe a
Nothing : Maybe a
Maybe : a -> Maybe a -- this can only be used in type signatures
Debido a la variable de tipo a , cualquier tipo puede "ajustarse dentro" del tipo Maybe . Entonces, Maybe Int , Maybe (List String) , o Maybe (Maybe (List Html)) , son todos tipos válidos. Al desestructurar cualquier valor de type con una expresión de case , debe tener en cuenta cada posible instanciación de ese tipo. En el caso de un valor de tipo Maybe a , debe tener en cuenta tanto el caso Just a case como el caso Nothing :
thing : Maybe Int
thing =
Just 3
blah : Int
blah =
case thing of
Just n ->
n
Nothing ->
42
-- blah = 3
Intente escribir el código anterior sin la cláusula Nothing en la expresión del case : no se compilará. Esto es lo que hace que el constructor de tipo Maybe un gran patrón para expresar valores que pueden no existir, ya que te obliga a manejar la lógica de cuando el valor es Nothing .
El nunca escribe
El tipo Never se puede construir (el módulo Basics no ha exportado su constructor de valores y tampoco le ha dado ninguna otra función que devuelva Never ). No hay ningún valor never : Never o una función createNever : ?? -> Never .
Esto tiene sus beneficios: puede codificar en un sistema de tipos una posibilidad que no puede ocurrir. Esto se puede ver en tipos como Task Never Int que garantiza que tendrá éxito con un Int ; o Program Never eso no tomará ningún parámetro al inicializar el código Elm desde JavaScript.
Variables Tipo Especial
Elm define las siguientes variables de tipo especial que tienen un significado particular para el compilador:
comparable: Compuesto porInt,Float,Char,Stringy tuples. Esto permite el uso de los operadores<y>.Ejemplo: Podría definir una función para encontrar los elementos más pequeños y más grandes en una lista (
extent). Piensas qué tipo de firma escribir. Por un lado, puede escribirextentInt : List Int -> Maybe (Int, Int)yextentChar : List Char -> Maybe (Char, Char)y otro paraFloatandString. La implementación de estos sería la misma: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) xsEs posible que tenga la tentación de simplemente escribir la
extent : List a -> Maybe (a, a), pero el compilador no le permitirá hacer esto, porque las funcionesminymaxno están definidas para estos tipos (NB: estos son solo envoltorios simples alrededor del<operador mencionado anteriormente). Puede resolver esto definiendo laextent : List comparable -> Maybe (comparable, comparable). Esto permite que su solución sea polimórfica , lo que significa que funcionará para más de un tipo.number: Compuesto porIntyFloat. Permite el uso de operadores aritméticos excepto división. Luego puede definir, por ejemplo, lasum : List number -> numbery hacer que funcione tanto para ints como para flotadores.appendable: compuesto porString,List. Permite el uso del operador++.compappend: esto aparece a veces, pero es un detalle de implementación del compilador. Actualmente esto no se puede utilizar en sus propios programas, pero a veces se menciona.
Tenga en cuenta que en una anotación de tipo como esta: number -> number -> number todos se refieren al mismo tipo, por lo que pasar Int -> Float -> Int sería un error de tipo. Puede resolver esto agregando un sufijo al tipo variable nombre: number -> number' -> number'' luego compilaría bien.
No hay un nombre oficial para estos, a veces se les llama:
- Variables Tipo Especial
- Variables de tipo tipo clase de clase
- Clases de seudo tipo
Esto se debe a que funcionan como las Clases de tipos de Haskell, pero sin que el usuario pueda definirlas.