Solo quiero señalar que los tipos de datos y la coincidencia de patrones (en una primera aproximación) son meramente útiles pero la característica del lenguaje redundante, que puede implementarse puramente utilizando el cálculo lambda. Si comprende cómo implementarlos en cálculo lambda, puede comprender por qué no necesitan Eq
para implementar la coincidencia de patrones.
La implementación de tipos de datos en cálculo lambda se conoce como "codificación eclesiástica" (después de Alonzo Church, que demostró cómo hacerlo). Las funciones codificadas por la iglesia también se conocen como "estilo de continuación de paso".
Se denomina "estilo de paso de continuación" porque en lugar de proporcionar el valor, proporciona una función que procesará el valor. Así, por ejemplo, en lugar de dar a un usuario una Int
, podría en lugar de darles un valor del siguiente tipo:
type IndirectInt = forall x . (Int -> x) -> x
El tipo anterior es "isomorfo" a un Int
. "Isomorfo" es sólo una forma elegante de decir que podemos convertir cualquier IndirectInt
en un Int
:
fw :: IndirectInt -> Int
fw indirect = indirect id
... y podemos convertir cualquier Int
en un IndirectInt
:
bw :: Int -> IndirectInt
bw int = \f -> f int
... tal que:
fw . bw = id -- Exercise: Prove this
bw . fw = id -- Exercise: Prove this
Uso de estilo de continuación de paso, que puede convertir cualquier tipo de datos en un plazo lambda-cálculo.Vamos a empezar con un tipo simple como:
data Either a b = Left a | Right b
En el estilo de continuación de paso, esto se convertiría en:
type IndirectEither a b = forall x . (Either a b -> x) -> x
Pero Alonzo Church era inteligente y se dio cuenta de que para cualquier tipo con varios constructores, sólo puede proporcionar una función separada para cada constructor. Así, en el caso del tipo anterior, en lugar de proporcionar una función del tipo (Either a b-> x)
, podemos en cambio ofrecemos dos funciones separadas, una para el a
, y una para el b
, y eso sería tan bueno:
type IndirectEither a b = forall x . (a -> x) -> (b -> x) -> x
-- Exercise: Prove that this definition is isomorphic to the previous one
¿Qué tal un tipo como Bool
donde los constructores no tienen argumentos? Bueno, Bool
es isomorfo a Either()()
(Ejercicio: Demostrar esto), por lo que sólo puede codificar como:
type IndirectBool = forall x . (() -> x) -> (() -> x) -> x
Y () -> x
es sólo isomorfo a x
(Ejercicio: Demostrar esto), así que podemos volver a escribir más como :
type IndirectBool = forall x . x -> x -> x
sólo hay dos funciones que pueden tener el tipo anterior:
true :: a -> a -> a
true a _ = a
false :: a -> a -> a
false _ a = a
Debido a la i somorphism, podemos garantizar que todas las codificaciones Church tendrán tantas implementaciones como valores posibles del tipo de datos original, por lo que no es coincidencia que existan exactamente dos funciones que habitan IndirectBool
, al igual que hay exactamente dos constructores para Bool
.
Pero, ¿cómo podemos coincidir con el patrón en nuestro IndirectBool
? Por ejemplo, con un ordinario Bool
, podríamos escribir:
expression1 :: a
expression2 :: a
case someBool of
True -> expression1
False -> expression2
Bueno, con nuestra IndirectBool
que ya cuentan con las herramientas para deconstruir sí. Nos gustaría simplemente escribir:
myIndirectBool expression1 expression2
Tenga en cuenta que si myIndirectBool
es true
, se recogerá la primera expresión, y si es false
que captará la segunda expresión, al igual que si tuviéramos alguna manera avenido patrón en su valor .
Tratemos de hacer lo mismo con un IndirectEither
. El uso de un ordinario Either
, escribíamos:
f :: a -> c
g :: b -> c
case someEither of
Left a -> f a
Right b -> g b
Con la IndirectEither
, nos acaba de escribir:
someIndirectEither f g
En resumen, cuando escribimos tipos de estilo de continuación de paso, son las continuaciones como los enunciados de caso de una construcción de caso, entonces todo lo que hacemos es pasar cada declaración de caso diferente como argumentos a la función.
Esta es la razón por la que no necesita ninguna sensación de Eq
para emparejar patrones en un tipo. En el cálculo lambda, el tipo decide qué es "igual" simplemente definiendo qué argumento selecciona de entre los proporcionados.Entonces, si soy un true
, pruebo que soy "igual" a true
seleccionando siempre mi primer argumento. Si soy false
, pruebo que soy "igual" a false
seleccionando siempre mi segundo argumento. En resumen, la "igualdad" del constructor se reduce a la "igualdad posicional", que siempre se define en un cálculo lambda, y si podemos distinguir un parámetro como el "primero" y otro como el "segundo", eso es todo lo que necesitamos tener la capacidad de "comparar" constructores.
No esperaba tocar los detalles hasta aquí cuando estaba pensando en mi pregunta, gracias por la iluminación. Tuve más de un "aha" al leer esto. Especialmente las definiciones de 'verdadero' y 'falso' hasta que vi el ejemplo 6 líneas a continuación lo hicieron totalmente claro. – epsilonhalbe
Si bien inicialmente podría parecer teórico y extravagante, esta coincidencia de patrones de estilo CPS se muestra a menudo en la práctica. Por ejemplo, Smalltalk no tiene sentencias if, pero los booleanos tienen métodos que reciben devoluciones de llamada para el caso verdadero y falso. – hugomg