Haskell Type vs Data Constructor

124

Estoy aprendiendo Haskell de learnyouahaskell.com . Tengo problemas para comprender los constructores de tipos y los constructores de datos. Por ejemplo, realmente no entiendo la diferencia entre esto:

data Car = Car { company :: String  
               , model :: String  
               , year :: Int  
               } deriving (Show) 

y esto:

data Car a b c = Car { company :: a  
                     , model :: b  
                     , year :: c   
                     } deriving (Show)  

Entiendo que el primero es simplemente usar un constructor ( Car) para construir datos de tipo Car. Realmente no entiendo el segundo.

Además, ¿cómo se definen los tipos de datos de esta manera:

data Color = Blue | Green | Red

encajar en todo esto?

Por lo que entiendo, el tercer ejemplo ( Color) es un tipo que puede estar en tres estados: Blue, Greeno Red. Pero eso entra en conflicto con la forma en que entiendo los dos primeros ejemplos: ¿es que el tipo Carsolo puede estar en un estado Car, que puede tomar varios parámetros para construir? Si es así, ¿cómo encaja el segundo ejemplo?

Esencialmente, estoy buscando una explicación que unifique los tres ejemplos / construcciones de código anteriores.

Aristides
fuente
18
Su ejemplo de Car puede ser un poco confuso porque Cares un constructor de tipos (en el lado izquierdo del =) y un constructor de datos (en el lado derecho). En el primer ejemplo, el Carconstructor de tipo no toma argumentos, en el segundo ejemplo toma tres. En ambos ejemplos, el Carconstructor de datos toma tres argumentos (pero los tipos de esos argumentos son en un caso fijos y en el otro parametrizados).
Simon Shine
el primero es simplemente usar un constructor de datos ( Car :: String -> String -> Int -> Car) para construir datos de tipo Car. el segundo es simplemente usar un constructor de datos ( Car :: a -> b -> c -> Car a b c) para construir datos de tipo Car a b c.
Will Ness

Respuestas:

228

En una datadeclaració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. Coloures un tipo y Greenes un constructor que contiene un valor de tipo Colour. Del mismo modo, Redy Blueson 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 RGBno es un valor, es una función que toma tres Ints y devuelve un valor. RGBtiene el tipo

RGB :: Int -> Int -> Int -> Colour

RGBes 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 RGBa tres valores, ¡obtenemos un valor de color!

Prelude> RGB 12 92 27
#0c5c1b

Hemos construido un valor de tipo Colouraplicando 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 Strings, puede imaginar hacer algo como

data SBTree = Leaf String
            | Branch String SBTree SBTree

Lo que vemos aquí es un tipo SBTreeque contiene dos constructores de datos. En otras palabras, hay dos funciones (a saber, Leafy Branch) que construirán valores del SBTreetipo. 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 Strings de alguna manera.

También vemos que ambos constructores de datos toman un Stringargumento: 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 SBTreey BBTreeson 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 BTreeha 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, Boolcomo argumento a BTree, devuelve el tipo BTree Bool, que es un árbol binario que almacena Bools. Reemplace cada aparición de la variable de tipo acon el tipo Bool, y puede ver por sí mismo cómo es cierto.

Si lo desea, puede ver BTreecomo una función con el tipo

BTree :: * -> *

Los tipos son algo así como tipos: *indica un tipo concreto, por lo que decimos que BTreees de un tipo concreto a un tipo concreto.

Terminando

Regrese aquí un momento y tome nota de las similitudes.

  • Un constructor de datos es una "función" que toma 0 o más valores y le devuelve un nuevo valor.

  • Un constructor de tipos es una "función" que toma 0 o más tipos y le devuelve un nuevo tipo.

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 atipo. Su definición es

data Maybe a = Nothing
             | Just a

Aquí, Maybehay un constructor de tipos que devuelve un tipo concreto. Justes un constructor de datos que devuelve un valor. Nothinges un constructor de datos que contiene un valor. Si miramos el tipo de Just, vemos que

Just :: a -> Maybe a

En otras palabras, Justtoma un valor de tipo ay devuelve un valor de tipo Maybe a. Si nos fijamos en el tipo de Maybe, vemos que

Maybe :: * -> *

En otras palabras, Maybetoma 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 Maybes, 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 Maybees una función de constructor de tipos, pero una lista debe contener valores de un tipo concreto. Maybe Inty Maybe ason tipos concretos (o si lo desea, llamadas a funciones de constructor de tipos que devuelven tipos concretos).

kqr
fuente
2
En su primer ejemplo, tanto ROJO VERDE como AZUL son constructores que no toman argumentos.
OllieB
3
La afirmación de que en data Colour = Red | Green | Blue"no tenemos ningún constructor en absoluto" es totalmente errónea. Los constructores de tipos y los constructores de datos no necesitan tomar argumentos, véase, por ejemplo, haskell.org/haskellwiki/Constructor, que señala que en data Tree a = Tip | Node a (Tree a) (Tree a)"hay dos constructores de datos, Tip y Node".
Frerich Raabe
1
@CMCDragonkai ¡Estás absolutamente en lo correcto! Los tipos son los "tipos de tipos". Un enfoque común para unir los conceptos de tipos y valores se llama escritura dependiente . Idris es un lenguaje de tipo dependiente inspirado en Haskell. Con las extensiones correctas de GHC, también puede acercarse un poco a la escritura dependiente en Haskell. (Algunas personas han estado bromeando que "la investigación Haskell se trata de encontrar la manera cerca de tipos dependientes podemos conseguir sin tener tipos dependientes.")
kqr
1
@CMCDragonkai En realidad, no es posible tener una declaración de datos vacía en el estándar Haskell. Pero hay una extensión GHC ( -XEmptyDataDecls) que te permite hacer eso. Como, como usted dice, no hay valores con ese tipo, una función f :: Int -> Zpuede, por ejemplo, no volver nunca (porque ¿qué devolvería?) Sin embargo, pueden ser útiles para cuando desea tipos pero realmente no le importan los valores .
kqr
1
Realmente no es posible? Acabo de probar en GHC, y lo ejecutó sin error. No tuve que cargar ninguna extensión de GHC, solo vanilla GHC. Entonces podría escribir :k Zy me dio una estrella.
CMCDragonkai
42

Haskell tiene tipos de datos algebraicos , que muy pocos otros idiomas tienen. Esto es quizás lo que te confunde.

En otros idiomas, generalmente puede hacer un "registro", "estructura" o similar, que tiene un montón de campos con nombre que contienen varios tipos diferentes de datos. También puede a veces hacer una "enumeración", que tiene una (pequeña) conjunto de valores fijos posibles (por ejemplo, tu Red, Greeny Blue).

En Haskell, puede combinar ambos al mismo tiempo. Extraño, pero cierto!

¿Por qué se llama "algebraico"? Bueno, los nerds hablan de "tipos de suma" y "tipos de producto". Por ejemplo:

data Eg1 = One Int | Two String

Un Eg1valor es, básicamente, ya sea un entero o una cadena. Entonces, el conjunto de todos los Eg1valores posibles es la "suma" del conjunto de todos los valores enteros posibles y todos los valores de cadena posibles. Por lo tanto, los nerds se refieren Eg1como un "tipo de suma". Por otra parte:

data Eg2 = Pair Int String

Cada Eg2valor consiste en tanto un número entero y una cadena. Por lo tanto, el conjunto de todos los Eg2valores posibles es el producto cartesiano del conjunto de todos los enteros y el conjunto de todas las cadenas. Los dos conjuntos se "multiplican" juntos, por lo que este es un "tipo de producto".

Los tipos algebraicos de Haskell son tipos de suma de tipos de productos . Le da a un constructor múltiples campos para hacer un tipo de producto, y tiene múltiples constructores para hacer una suma (de productos).

Como ejemplo de por qué eso podría ser útil, suponga que tiene algo que genera datos como XML o JSON, y requiere un registro de configuración, pero obviamente, las configuraciones de configuración para XML y JSON son totalmente diferentes. Entonces podrías hacer algo como esto:

data Config = XML_Config {...} | JSON_Config {...}

(Con algunos campos adecuados allí, obviamente). No puede hacer cosas como esta en lenguajes de programación normales, por lo que la mayoría de la gente no está acostumbrada.

Orquídea matemática
fuente
44
¡Excelente! solo una cosa, "Pueden ... construirse en casi cualquier idioma", dice Wikipedia . :) En, por ejemplo, C / ++, eso es unions, con una disciplina de etiqueta. :)
Will Ness
55
Sí, pero cada vez que menciono union, la gente me mira como "¿quién diablos usa eso ?" ;-)
MathematicalOrchid
1
He visto muchos unionusos en mi carrera en C. No hagas que parezca innecesario porque ese no es el caso.
truthadjustr
26

Comience con el caso más simple:

data Color = Blue | Green | Red

Esto define un "constructor de tipos" Colorque no toma argumentos, y tiene tres "constructores de datos" Blue, Greeny Red. Ninguno de los constructores de datos toma ningún argumento. Esto significa que hay tres de tipo Color: Blue, Greeny Red.

Se utiliza un constructor de datos cuando necesita crear un valor de algún tipo. Me gusta:

myFavoriteColor :: Color
myFavoriteColor = Green

crea un valor myFavoriteColorutilizando el Greenconstructor de datos, y myFavoriteColorserá de tipo Colorya que ese es el tipo de valores producidos por el constructor de datos.

Se utiliza un constructor de tipos cuando necesita crear un tipo de algún tipo. Este suele ser el caso al escribir firmas:

isFavoriteColor :: Color -> Bool

En este caso, está llamando al Colorconstructor de tipos (que no toma argumentos).

¿Aún conmigo?

Ahora, imagine que no solo desea crear valores rojos / verdes / azules sino que también desea especificar una "intensidad". Como, un valor entre 0 y 256. Puede hacerlo agregando un argumento a cada uno de los constructores de datos, de modo que termine con:

data Color = Blue Int | Green Int | Red Int

Ahora, cada uno de los tres constructores de datos toma un argumento de tipo Int. El constructor de tipos ( Color) todavía no toma ningún argumento. Entonces, mi color favorito es un verde oscuro, podría escribir

    myFavoriteColor :: Color
    myFavoriteColor = Green 50

Y nuevamente, llama al Greenconstructor de datos y obtengo un valor de tipo Color.

Imagínese si no quiere dictar cómo las personas expresan la intensidad de un color. Algunos pueden querer un valor numérico como acabamos de hacer. Otros pueden estar bien con solo un booleano que indica "brillante" o "no tan brillante". La solución a esto es no codificar Inten los constructores de datos, sino usar una variable de tipo:

data Color a = Blue a | Green a | Red a

Ahora, nuestro constructor de tipos toma un argumento (¡otro tipo que acabamos de llamar a!) Y todos los constructores de datos tomarán un argumento (¡un valor!) De ese tipo a. Entonces podrías tener

myFavoriteColor :: Color Bool
myFavoriteColor = Green False

o

myFavoriteColor :: Color Int
myFavoriteColor = Green 50

Observe cómo llamamos al Colorconstructor de tipos con un argumento (otro tipo) para obtener el tipo "efectivo" que será devuelto por los constructores de datos. Esto toca el concepto de tipos sobre los que es posible que desee leer con una taza de café o dos.

Ahora descubrimos qué son los constructores de datos y los constructores de tipos, y cómo los constructores de datos pueden tomar otros valores como argumentos y los constructores de tipos pueden tomar otros tipos como argumentos. HTH

Frerich Raabe
fuente
No estoy seguro de ser amigo de su noción de un constructor de datos nulo. Sé que es una forma común de hablar sobre las constantes en Haskell, pero ¿no se ha demostrado que es incorrecto varias veces?
kqr
@kqr: un constructor de datos puede ser nulo, pero ya no es una función. Una función es algo que toma un argumento y produce un valor, es decir, algo con ->la firma.
Frerich Raabe
¿Puede un valor apuntar a múltiples tipos? ¿O cada valor está asociado con solo 1 tipo y listo?
CMCDragonkai
1
@jrg Hay cierta superposición, pero no se debe específicamente a los constructores de tipos sino a las variables de tipo, por ejemplo, ain data Color a = Red a. aes un marcador de posición para un tipo arbitrario. Sin embargo, puede tener lo mismo en funciones simples, por ejemplo, una función de tipo (a, b) -> atoma una tupla de dos valores (de tipos ay b) y produce el primer valor. Es una función "genérica", ya que no dicta el tipo de los elementos de la tupla, solo especifica que la función produce un valor del mismo tipo que el primer elemento de la tupla.
Frerich Raabe
1
+1 Now, our type constructor takes one argument (another type which we just call a!) and all of the data constructors will take one argument (a value!) of that type a.Esto es muy útil.
Jonas
5

Como otros señalaron, el polimorfismo no es tan útil aquí. Veamos otro ejemplo con el que probablemente ya estés familiarizado:

Maybe a = Just a | Nothing

Este tipo tiene dos constructores de datos. Nothinges algo aburrido, no contiene datos útiles. Por otro lado, Justcontiene un valor de acualquier tipo que apueda tener. Escribamos una función que use este tipo, por ejemplo, obtener el encabezado de una Intlista, si hay alguna (espero que esté de acuerdo en que esto es más útil que arrojar un error):

maybeHead :: [Int] -> Maybe Int
maybeHead [] = Nothing
maybeHead (x:_) = Just x

> maybeHead [1,2,3]    -- Just 1
> maybeHead []         -- None

Entonces, en este caso aes un Int, pero funcionaría tan bien para cualquier otro tipo. De hecho, puede hacer que nuestra función funcione para cada tipo de lista (incluso sin cambiar la implementación):

maybeHead :: [t] -> Maybe t
maybeHead [] = Nothing
maybeHead (x:_) = Just x

Por otro lado, puede escribir funciones que aceptan solo un cierto tipo de Maybe, por ejemplo,

doubleMaybe :: Maybe Int -> Maybe Int
doubleMaybe Just x = Just (2*x)
doubleMaybe Nothing= Nothing

En resumen, con el polimorfismo, le da a su propio tipo la flexibilidad para trabajar con valores de otros tipos diferentes.

En su ejemplo, puede decidir en algún momento que Stringno es suficiente para identificar a la empresa, pero que debe tener su propio tipo Company(que contiene datos adicionales como el país, la dirección, las cuentas pendientes, etc.). Su primera implementación de Carnecesitaría cambiar para usar en Companylugar de Stringpara su primer valor. Su segunda implementación está bien, la usa como Car Company String Inty funcionaría como antes (por supuesto, las funciones de acceso a los datos de la empresa deben cambiarse).

Landei
fuente
¿Puedes usar constructores de tipos en el contexto de datos de otra declaración de datos? Algo como data Color = Blue ; data Bright = Color? Lo probé en ghci, y parece que el Color en el constructor de tipos no tiene nada que ver con el constructor de datos de Color en la definición de Bright. Solo hay 2 constructores de color, uno que es Data y el otro es Type.
CMCDragonkai
@CMCDragonkai No creo que puedas hacer esto, y ni siquiera estoy seguro de lo que quieres lograr con esto. Puede "ajustar" un tipo existente usando datao newtype(por ejemplo data Bright = Bright Color), o puede usarlo typepara definir un sinónimo (por ejemplo type Bright = Color).
Landei
5

El segundo tiene la noción de "polimorfismo".

El a b cpuede ser de cualquier tipo. Por ejemplo, apuede ser un [String], bpuede ser [Int] y cpuede ser [Char].

Mientras que el tipo del primero es fijo: la compañía es una String, el modelo es una Stringy el año es Int.

El ejemplo de Car podría no mostrar la importancia del uso del polimorfismo. Pero imagine que sus datos son del tipo de lista. Una lista puede contener String, Char, Int ...En esas situaciones, necesitará la segunda forma de definir sus datos.

En cuanto a la tercera forma, no creo que deba encajar en el tipo anterior. Es solo otra forma de definir datos en Haskell.

Esta es mi humilde opinión como principiante.

Por cierto: asegúrese de entrenar bien su cerebro y sentirse cómodo con esto. Es la clave para entender a Monad más tarde.

McBear Holden
fuente
1

Se trata de tipos : en el primer caso, establezca los tipos String(por empresa y modelo) y Intpor año. En el segundo caso, eres más genérico. a,, by cpueden ser los mismos tipos que en el primer ejemplo, o algo completamente diferente. Por ejemplo, puede ser útil dar el año como cadena en lugar de entero. Y si lo desea, incluso puede usar su Colortipo.

Matías
fuente