En una data
declaración, un constructor de tipo es lo que está en el lado izquierdo del signo igual. Los constructores de datos son las cosas en el lado derecho del signo igual. Utiliza constructores de tipos donde se espera un tipo y usa constructores de datos donde se espera un valor.
Constructores de datos
Para simplificar las cosas, podemos comenzar con un ejemplo de un tipo que representa un color.
data Colour = Red | Green | Blue
Aquí tenemos tres constructores de datos. Colour
es un tipo y Green
es un constructor que contiene un valor de tipo Colour
. Del mismo modo, Red
y Blue
son constructores que construyen valores de tipo Colour
. ¡Aunque podríamos imaginar condimentarlo!
data Colour = RGB Int Int Int
Todavía tenemos el tipo Colour
, pero RGB
no es un valor, es una función que toma tres Ints y devuelve un valor. RGB
tiene el tipo
RGB :: Int -> Int -> Int -> Colour
RGB
es un constructor de datos que es una función que toma algunos valores como argumentos, y luego los usa para construir un nuevo valor. Si ha realizado alguna programación orientada a objetos, debe reconocer esto. ¡En OOP, los constructores también toman algunos valores como argumentos y devuelven un nuevo valor!
En este caso, si aplicamos RGB
a tres valores, ¡obtenemos un valor de color!
Prelude> RGB 12 92 27
#0c5c1b
Hemos construido un valor de tipo Colour
aplicando el constructor de datos. Un constructor de datos contiene un valor como lo haría una variable o toma otros valores como argumento y crea un nuevo valor . Si ha realizado una programación previa, este concepto no debería serle muy extraño.
Descanso
Si desea construir un árbol binario para almacenar String
s, puede imaginar hacer algo como
data SBTree = Leaf String
| Branch String SBTree SBTree
Lo que vemos aquí es un tipo SBTree
que contiene dos constructores de datos. En otras palabras, hay dos funciones (a saber, Leaf
y Branch
) que construirán valores del SBTree
tipo. Si no está familiarizado con el funcionamiento de los árboles binarios, simplemente espere allí. En realidad, no necesita saber cómo funcionan los árboles binarios, solo que este almacena String
s de alguna manera.
También vemos que ambos constructores de datos toman un String
argumento: esta es la cadena que van a almacenar en el árbol.
¡Pero! Y si también quisiéramos poder almacenar Bool
, tendríamos que crear un nuevo árbol binario. Podría verse más o menos así:
data BBTree = Leaf Bool
| Branch Bool BBTree BBTree
Constructores tipo
Ambos SBTree
y BBTree
son constructores tipo. Pero hay un problema evidente. ¿Ves lo similares que son? Esa es una señal de que realmente quieres un parámetro en alguna parte.
Entonces podemos hacer esto:
data BTree a = Leaf a
| Branch a (BTree a) (BTree a)
Ahora presentamos una variable de tipo a
como parámetro para el constructor de tipos. En esta declaración, se BTree
ha convertido en una función. Toma un tipo como argumento y devuelve un nuevo tipo .
Aquí es importante considerar la diferencia entre un tipo concreto (los ejemplos incluyen Int
, [Char]
y Maybe Bool
) que es un tipo que puede asignarse a un valor en su programa, y una función de constructor de tipo que necesita alimentar un tipo para poder ser asignado a un valor. Un valor nunca puede ser del tipo "lista", porque debe ser una "lista de algo ". En el mismo espíritu, un valor nunca puede ser del tipo "árbol binario", porque debe ser un "árbol binario que almacene algo ".
Si pasamos, digamos, Bool
como argumento a BTree
, devuelve el tipo BTree Bool
, que es un árbol binario que almacena Bool
s. Reemplace cada aparición de la variable de tipo a
con el tipo Bool
, y puede ver por sí mismo cómo es cierto.
Si lo desea, puede ver BTree
como una función con el tipo
BTree :: * -> *
Los tipos son algo así como tipos: *
indica un tipo concreto, por lo que decimos que BTree
es de un tipo concreto a un tipo concreto.
Terminando
Regrese aquí un momento y tome nota de las similitudes.
Los constructores de datos con parámetros son geniales si queremos ligeras variaciones en nuestros valores: colocamos esas variaciones en los parámetros y dejamos que el tipo que crea el valor decida qué argumentos van a presentar. En el mismo sentido, los constructores de tipos con parámetros son geniales si queremos ligeras variaciones en nuestros tipos! Ponemos esas variaciones como parámetros y dejamos que el tipo que crea el tipo decida qué argumentos van a presentar.
Un caso de estudio
A medida que la casa se extiende aquí, podemos considerar el Maybe a
tipo. Su definición es
data Maybe a = Nothing
| Just a
Aquí, Maybe
hay un constructor de tipos que devuelve un tipo concreto. Just
es un constructor de datos que devuelve un valor. Nothing
es un constructor de datos que contiene un valor. Si miramos el tipo de Just
, vemos que
Just :: a -> Maybe a
En otras palabras, Just
toma un valor de tipo a
y devuelve un valor de tipo Maybe a
. Si nos fijamos en el tipo de Maybe
, vemos que
Maybe :: * -> *
En otras palabras, Maybe
toma un tipo concreto y devuelve un tipo concreto.
¡Una vez más! La diferencia entre un tipo concreto y una función constructora de tipos. No puede crear una lista de Maybe
s, si intenta ejecutar
[] :: [Maybe]
Obtendrás un error. Sin embargo, puede crear una lista de Maybe Int
, o Maybe a
. Esto se debe a que Maybe
es una función de constructor de tipos, pero una lista debe contener valores de un tipo concreto. Maybe Int
y Maybe a
son tipos concretos (o si lo desea, llamadas a funciones de constructor de tipos que devuelven tipos concretos).
Car
es un constructor de tipos (en el lado izquierdo del=
) y un constructor de datos (en el lado derecho). En el primer ejemplo, elCar
constructor de tipo no toma argumentos, en el segundo ejemplo toma tres. En ambos ejemplos, elCar
constructor de datos toma tres argumentos (pero los tipos de esos argumentos son en un caso fijos y en el otro parametrizados).