opciones - pattern matching haskell
¿Qué es() en Haskell, exactamente? (6)
Estoy leyendo Learn You a Haskell , y en los capítulos de mónadas, me parece que ()
está siendo tratado como una especie de "nulo" para cada tipo. Cuando compruebo el tipo de ()
en GHCi, obtengo
>> :t ()
() :: ()
que es una declaración extremadamente confusa. Parece que ()
es un tipo para sí mismo. Estoy confundido sobre cómo encaja en el lenguaje y cómo parece ser capaz de representar cualquier tipo.
El tipo ()
se puede considerar como una tupla de elemento cero. Es un tipo que solo puede tener un valor, y por lo tanto se usa donde necesita un tipo, pero en realidad no es necesario que transmita ninguna información. Aquí hay un par de usos para esto.
Las cosas monádicas como IO
y State
tienen un valor de retorno, así como también la realización de efectos secundarios. A veces, el único punto de la operación es realizar un efecto secundario, como escribir en la pantalla o almacenar algún estado. Para escribir en la pantalla, putStrLn
debe tener el tipo String -> IO ?
- IO
siempre debe tener algún tipo de devolución, pero aquí no hay nada útil que devolver. Entonces, ¿qué tipo deberíamos devolver? Podríamos decir Int, y siempre devolver 0, pero eso es engañoso. Entonces devolvemos ()
, el tipo que tiene solo un valor (y por lo tanto no hay información útil), para indicar que no hay nada útil que regrese.
A veces es útil tener un tipo que no puede tener valores útiles. Considere si ha implementado un tipo de Map kv
que asigna claves de tipo k
a valores de tipo v
. Entonces desea implementar un Set
, que es realmente similar a un mapa, excepto que no necesita la parte de valor, solo las teclas. En un lenguaje como Java, puede usar booleanos como el tipo de valor ficticio, pero realmente solo desea un tipo que no tenga valores útiles. Entonces podrías decir type Set k = Map k ()
Debe notarse que ()
no es particularmente mágico. Si lo desea, puede almacenarlo en una variable y hacer una coincidencia de patrón en él (aunque no tiene mucho sentido):
main = do
x <- putStrLn "Hello"
case x of
() -> putStrLn "The only value..."
La confusión proviene de otros lenguajes de programación: "vacío" significa en la mayoría de los lenguajes imperativos que no hay estructura en la memoria que almacena un valor. Parece inconsistente porque "boolean" tiene 2 valores en lugar de 2 bits, mientras que "void" no tiene bits en lugar de ningún valor, pero ahí se trata de lo que devuelve una función en un sentido práctico. Para ser exactos: su valor único no consume ni un poco de almacenamiento.
Vamos a ignorar el valor inferior (escrito _|_
) por un momento ...
()
se llama Unidad, escrita como una nula-tupla. Tiene solo un valor. Y no se llama Void
, porque Void
no tiene ningún valor, por lo que no podría ser devuelto por ninguna función.
Observe esto: Bool
tiene 2 valores ( True
y False
), ()
tiene un valor ( ()
) y el Void
no tiene valor (no existe). Son como conjuntos con dos / uno / sin elementos. La menor memoria que necesitan para almacenar su valor es 1 bit / no bit / impossible, respectivamente. Lo que significa que una función que devuelve a ()
puede regresar con un valor de resultado (el obvio) que puede ser inútil para usted. Void
otro lado, Void
implicaría que esa función nunca volverá y nunca dará ningún resultado, porque no existiría ningún resultado.
Si desea darle un nombre a "ese valor", devuelve una función que nunca regresa (sí, esto suena como una conversación loca), luego llámela abajo (" _|_
", escrita como una T invertida). Podría representar una excepción o un bucle infinito o un punto muerto o "simplemente espere más". (Algunas funciones solo volverán a la parte inferior, si uno de sus parámetros está abajo).
Cuando crea el producto cartesiano / una tupla de estos tipos, observará el mismo comportamiento: (Bool,Bool,Bool,(),())
tiene 2 · 2 · 2 · 1 · 1 = 6 valores diferentes. (Bool,Bool,Bool,(),Void)
es como el conjunto {t, f} × {t, f} × {t, f} × {u} × {} que tiene 2 · 2 · 2 · 1 · 0 = 0 elementos, a menos que cuente _|_
como un valor.
Otro ángulo más:
()
es el nombre de un conjunto que contiene un único elemento llamado ()
.
Es de hecho un poco confuso que el nombre del conjunto y el elemento en el mismo pasa a ser el mismo en este caso.
Recuerde: en Haskell un tipo es un conjunto que tiene sus posibles valores como elementos.
Realmente me gusta pensar en ()
por analogía con las tuplas.
(Int, Char)
es el tipo de todos los pares de un Int
y un Char
, por lo que sus valores son todos los valores posibles de Int
cruzados con todos los valores posibles de Char
. (Int, Char, String)
es similar al tipo de todos los triples de un Int
, un Char
y un String
.
Es fácil ver cómo seguir extendiendo este patrón hacia arriba, pero ¿qué hay de hacia abajo?
(Int)
sería el tipo "1-tupla", que consta de todos los valores posibles de Int
. Pero eso sería analizado por Haskell simplemente poniendo paréntesis alrededor de Int
, y siendo así solo el tipo Int
. Y los valores de este tipo serían (1)
, (2)
, (3)
, etc., que también se analizarían como valores Int
normales entre paréntesis. Pero si lo piensas, una "1-tupla" es exactamente lo mismo que un solo valor, por lo que no es necesario que realmente existan.
Bajando un paso más hacia cero-tuplas nos da ()
, que deberían ser todas las posibles combinaciones de valores en una lista vacía de tipos. Bueno, hay exactamente una forma de hacerlo, que es no contener ningún otro valor, por lo que debe haber un solo valor en el tipo ()
. Y por analogía con la sintaxis del valor de tupla, podemos escribir ese valor como ()
, que sin duda se parece a una tupla que no contiene valores.
Así es exactamente como funciona. No hay magia, y este tipo ()
y su valor ()
son tratados de ninguna manera por el lenguaje.
()
no se trata de hecho como "un valor nulo para ningún tipo" en los ejemplos de mónadas en el libro LYAH. Siempre que se use el tipo ()
el único valor que se puede devolver es ()
. Entonces se usa como un tipo para decir explícitamente que no puede haber ningún otro valor de retorno. Y del mismo modo en que se supone que debe devolverse otro tipo, no puede regresar ()
.
Lo que hay que tener en cuenta es que cuando un conjunto de cálculos monádicos se componen junto con bloques o operadores como >>=
, >>
, etc., construirán un valor de tipo ma
para alguna mónada m
. Esa elección de m
tiene que permanecer igual a través de los componentes (no hay forma de componer un Maybe Int
con un IO Int
de esa manera), pero el a
puede y muy a menudo es diferente en cada etapa.
Entonces, cuando alguien pega un IO ()
en medio de un cálculo de IO String
, eso no usa el ()
como un valor nulo en el tipo String
, simplemente está usando un IO ()
en el camino para construir una IO String
, de la misma manera podrías usar un Int
en el camino para construir una String
.
Se llama tipo de Unit
, generalmente se usa para representar los efectos secundarios. Puedes pensarlo vagamente como Void
en Java. Lea más here y here etc. Lo que puede ser confuso es que ()
representa sintácticamente tanto el tipo como su único valor literal. También tenga en cuenta que no es similar a null
en Java, lo que significa que una referencia indefinida - ()
es efectivamente una tupla de 0 tamaños.
tl; dr ()
no agrega un valor "nulo" a cada tipo, infierno no; ()
es un valor "aburrido" en un tipo propio: ()
.
Permítanme retroceder un momento en la pregunta y abordar una fuente común de confusión. Una cosa clave para absorber al aprender Haskell es la distinción entre su lenguaje de expresión y su lenguaje de tipo . Probablemente estés al tanto de que los dos se mantienen separados. Pero eso permite que se use el mismo símbolo en ambos, y eso es lo que está sucediendo aquí. Hay simples indicaciones textuales para decirte qué idioma estás mirando. No necesita analizar todo el lenguaje para detectar estas señales.
El nivel superior de un módulo Haskell vive, por defecto, en el lenguaje de expresión. Usted define funciones escribiendo ecuaciones entre expresiones. Pero cuando ve foo :: bar en el lenguaje de expresión, significa que foo es una expresión y la barra es su tipo. Entonces cuando lee () :: ()
, está viendo una declaración que relaciona el ()
en el lenguaje de expresión con el ()
en el lenguaje de tipo. Los dos ()
símbolos significan cosas diferentes, porque no están en el mismo idioma. Esta repetición a menudo causa confusión para los principiantes, hasta que la separación de lenguaje de expresión / tipo se instala en su subconsciente, momento en el que se convierte en útil mnemotécnico.
Los data
palabras clave introducen una nueva declaración de tipo de datos, que implica una combinación cuidadosa de los idiomas de expresión y de tipo, ya que dice primero cuál es el nuevo tipo y, en segundo lugar, cuáles son sus valores.
data TyCon tyvar ... tyvar = ValCon1 type ... type | ... | ValConn type ... type
En una declaración de este tipo, el constructor de tipo TyCon se agrega al lenguaje de tipo y los constructores de valor de ValCon se agregan al lenguaje de expresiones (y su sublengua de patrones). En una declaración de data
, las cosas que están en el argumento coloca para ValCon s le dicen los tipos dados a los argumentos cuando esa ValCon se usa en expresiones. Por ejemplo,
data Tree a = Leaf | Node (Tree a) a (Tree a)
declara un Tree
constructor de tipo para tipos de árbol binario que almacena elementos en nodos, cuyos valores están dados por los constructores de valor Leaf
y Node
. Me gusta colorear los constructores de tipo (Árbol) azul y los constructores de valor (Hoja, Nodo) rojo. No debe haber azul en las expresiones y (a menos que use funciones avanzadas) no hay rojo en los tipos. El tipo incorporado Bool
podría ser declarado,
data Bool = True | False
agregando Bool
azul al idioma de Bool
y rojo True
y False
al idioma de la expresión. Tristemente, mi markdown-fu es inadecuado para la tarea de agregar los colores a esta publicación, por lo que solo tendrá que aprender a agregar los colores en su cabeza.
El tipo "unidad" usa ()
como un símbolo especial, pero funciona como si estuviera declarado
data () = () -- the left () is blue; the right () is red
lo que significa que un azul teóricamente ()
es un constructor de tipos en el lenguaje de tipos, pero que un rojo teóricamente ()
es un constructor de valores en el lenguaje de expresiones, y de hecho () :: ()
. [No es el único ejemplo de tal juego de palabras. Los tipos de tuplas más grandes siguen el mismo patrón: la sintaxis de pares es como si estuviera dada por
data (a, b) = (a, b)
agregando (,) a ambos tipos y lenguajes de expresión. Pero yo divago.
Entonces el tipo ()
, a menudo pronunciado "Unidad", es un tipo que contiene un valor del que vale la pena hablar: ese valor está escrito ()
pero en el lenguaje de expresión, y algunas veces se pronuncia "vacío". Un tipo con un solo valor no es muy interesante. Un valor de tipo ()
aporta cero bits de información: ya sabes lo que debe ser. Entonces, aunque no hay nada especial sobre type ()
para indicar efectos secundarios, a menudo aparece como el componente de valor en un tipo monádico. Las operaciones monádicas tienden a tener tipos que se parecen a
val-in-type-1 -> ... -> val-in-type-n -> effect-monad val-out-type
donde el tipo de devolución es una aplicación de tipo: la función te dice qué efectos son posibles y el argumento te dice qué tipo de valor produce la operación. Por ejemplo
put :: s -> State s ()
que se lee (porque la aplicación se asocia a la izquierda ["como todos lo hicimos en los años sesenta", Roger Hindley]) como
put :: s -> (State s) ()
tiene un tipo de entrada de valor s
, el State s
mónada de efecto y el tipo de salida de valor ()
. Cuando ve ()
como un tipo de salida de valor, eso solo significa que "esta operación se usa solo para su efecto , el valor entregado no es interesante". similar
putStr :: String -> IO ()
entrega una cadena a stdout
pero no devuelve nada emocionante.
El tipo ()
también es útil como un tipo de elemento para estructuras similares a contenedores, donde indica que los datos consisten solo en una forma , sin carga útil interesante. Por ejemplo, si Tree
se declara como arriba, entonces Tree ()
es el tipo de formas de árbol binario, que no almacena nada de interés en los nodos. Del mismo modo [()]
es el tipo de listas de elementos aburridos, y si no hay nada de interés en los elementos de una lista, entonces la única información que aporta es su longitud.
En resumen, ()
es un tipo. Su único valor, ()
, tiene el mismo nombre, pero está bien porque los idiomas de tipo y expresión están separados. Es útil tener un tipo que represente "sin información" porque, en contexto (por ejemplo, de una mónada o un contenedor), te dice que solo el contexto es interesante.