Leyendo tipos
En la sección Fundamentos del lenguaje revisamos varios ejemplos interactivos para desarrollar una intuición general del lenguaje. Ahora vamos a volver a hacer lo mismo, pero con una nueva pregunta en mente: ¿Qué tipo de valor es este?
Valores primitivos y listas
Ingresemos algunas expresiones simples y veamos qué pasa:
Haz clic sobre esta caja negra ⬆️ y verás un cursor parpadeando. Escribe 3.1415
y apreta “enter”. Debería aparecer 3.1415
seguido del tipo Float
.
Okay, pero ¿qué es esto, exactamente? Cada fila muestra un valor junto con el tipo que le corresponde al valor. Puedes leer estos ejemplos de esta forma:
- El valor
"hello"
es unString
. - El valor
False
es unBool
. - El valor
3
es unInt
. - El valor
3.1415
es unFloat
.
Elm reconoce el tipo de cualquier valor que ingresemos. Veamos qué pasa con listas:
Puedes leer estos tipos de esta forma:
- Tenemos una
List
rellena con valoresString
. - Tenemos una
List
rellena con valoresFloat
.
El tipo es una descripción general del valor particular que estamos viendo.
Funciones
Veamos el tipo de algunas funciones:
Ingresa round
o sqrt
para ver otros tipos de funciones ⬆️
La función String.length
tiene el tipo String -> Int
. Esto significa que tiene que recibir un argumento String
, y que definitivamente retornará un valor Int
. Probemos pasarle un argumento:
Le pasamos un argumento String
a una función String -> Int
. Esto resulta en un Int
.
¿Y qué ocurre si le pasamos algo que no es String
? Prueba escribir String.length [1, 2, 3]
o String.length True
y ve lo que ocurre ⬆️
Vas a darte cuenta de que una función String -> Int
debe sí o sí recibir un argumento String
.
Nota: Las funciones que reciben múltiples argumentos se escriben con varias flechas. Por ejemplo, esta es una función que recibe dos argumentos:
[ { "input": "String.repeat", "value": "\u001b[36m<function>\u001b[0m", "type_": "Int -> String -> String" } ]Si le pasas dos argumentos, así:
String.repeat 3 "ha"
, el resultado será"hahaha"
. Puedes considerar->
como una forma rara de separar los argumentos, pero la razón de por qué se escribe así la explico aquí. ¡Es bastante interesante!
Anotaciones de tipo
Hasta ahora hemos permitido que Elm infiera el tipado, pero también podemos anotar tipos en la línea justo arriba de una definición. Es decir, podemos escribir algo así:
half : Float -> Float
half n =
n / 2
-- half 256 == 128
-- half "3" -- error!
hypotenuse : Float -> Float -> Float
hypotenuse a b =
sqrt (a ^ 2 + b ^ 2)
-- hypotenuse 3 4 == 5
-- hypotenuse 5 12 == 13
checkPower : Int -> String
checkPower powerLevel =
if powerLevel > 9000 then
"It's over 9000!!!"
else
"Meh"
-- checkPower 9001 == "It's over 9000!!!"
-- checkPower True -- error!
No es necesario anotar tipos, pero definitivamente lo recomiendo. Estos son algunos beneficios:
- Calidad de los mensajes de error — Cuando anotamos un tipo, le estamos diciendo al compilador nuestra intención. La implementación puede que tenga errores, y después de comparar el código con nuestra anotación, el compilador nos dará un mensaje del tipo: “Dijiste que el argumento
powerLevel
eraInt
, pero está siendo usado comoString
”. - Documentación — Cuando pase un tiempo sin que trabajemos sobre el mismo código, o cuando un colega lo haga por primera vez, va a ser muy útil ver exactamente lo que recibe y devuelve una función sin tener que leer la implementación en detalle.
Pero la gente igual puede cometer errores al anotar tipos, así que ¿qué pasa si la anotación no coincide con la implementación? Pues, el compilador determina todos los tipos por su cuenta y confirma que tu anotación coincide con la respuesta real. En otras palabras, el compilador siempre verificará que todas las anotaciones que escribas estén correctas. Así tendrás mejores mensajes de error y además tu documentación se mantendrá siempre al día.
Variables de tipo
A medida que revises más código Elm, te irás dando cuenta de que existen anotaciones de tipo con letras en minúscula. Un ejemplo común es el de la función List.length
:
Fíjate en esa a
minúscula en el tipo. Esto se llama una variable de tipo. Su significado puede cambiar según cómo se use List.length
:
Como sólo queremos el largo de la lista, no nos importa qué lleva dentro. La variable de tipo a
significa que ahí puede ir cualquier tipo. Veamos otro ejemplo común:
Otra vez, la variable de tipo a
puede cambiar según cómo usemos List.reverse
. Pero en este caso, tenemos una a
tanto en el argumento como en el tipo de retorno. Esto significa que si le pasas una List Int
, deberás recibir una List Int
también. Una vez que se decide lo que es esa a
, seguirá siendo lo mismo después.
Nota: Las variables de tipo deben empezar con una letra minúscula, pero pueden ser palabras completas. Podemos escribir el tipo de
List.length
comoList value -> Int
y podríamos escribir el tipo deList.reverse
comoList element -> List element
. Funciona siempre y cuando comiencen con una letra minúscula. Las variables de tipoa
yb
son usadas por convención en muchos lugares, pero algunas anotaciones quedan mejor con nombres más específicos.
Variables limitadas de tipo
Hay una variedad especial de variable de tipo en Elm que llamamos variables limitadas de tipo. El ejemplo más común es el tipo number
. La función negate
la usa:
Prueba escribir expresiones como negate 3.1415
o negate (round 3.1415)
o negate "hi"
⬆️
Normalmente, las variables de tipo pueden rellenarse con cualquier cosa, pero number
sólo puede rellenarse con valores Int
y Float
. O sea, limita las posibilidades.
Esta es la lista completa de variables limitadas de tipo:
number
permiteInt
yFloat
appendable
permiteString
yList a
comparable
permiteInt
,Float
,Char
,String
y listas o tuplas de valorescomparable
compappend
permiteString
yList comparable
Estas variables limitadas de tipo existen para que ciertos operadores como (+)
y (<)
puedan ser un poco más flexibles.
Ya cubrimos bastante bien los tipos de valores y funciones, pero ¿cómo se ve esto cuando empezamos a necesitar estructuras más complejas de datos?