2012-02-02 10 views
7

Esto es algo así como una pregunta filosófica, pero espero haber sido respondida por documentación oficial o "palabra de dios" (léase: SPJ). ¿Hay alguna razón específica por la cual el comité Haskell eligió requerir interfaces explícitas en forma de clases de tipos en lugar de una solución más uniforme basada en la coincidencia de patrones?¿Por qué tipo de clases en lugar de simplemente coincidencia de patrones?

A modo de ejemplo, tomar Eq:

class Eq a where 
    (==), (/=) :: a -> a -> Bool 
    x == y = not $ x /= y 
    x /= y = not $ x == y 

instance Eq Int where 
    (==) = internalIntEq 

qué nosotros no pudimos hacer algo como esto en su lugar (oso con el pseudo-Haskell):

(==), (/=) :: a -> a -> Bool 
default x == y = not $ x /= y    -- 1 
default x /= y = not $ x == y 

(Int a) == (Int b) = a `internalIntEq` b -- 2 

Es decir, si Haskell fueron a permitir la coincidencia de patrones de los tipos de datos ordinarios, entonces:

  • Los programadores podrían crear clases ad-hoc, es decir, instance serían implícita (2)

  • Tipos todavía podía inferirse y emparejados estáticamente (SupportsEqualsEquals a => ...)

  • implementaciones predeterminadas vendría “gratis”

  • Clases podría fácilmente extenderse sin romper nada

Tendría que haber una manera de especificar un patrón predeterminado (1) que, aunque declarado antes de otro s, siempre coincide con el último. ¿Alguna de estas características hipotéticas choca con algo inherente a Haskell? ¿Sería difícil o imposible inferir correctamente los tipos? Parece ser una característica poderosa que se combinaría muy bien con el resto de Haskell, así que creo que hay una buena razón por la que no lo hacemos así. ¿Es este mecanismo de polimorfismo ad-hoc simplemente también ad-hoc?

+0

No soy muy versado en esto, pero creo que uno de los principales problemas es que pierdes parametricidad y, por lo tanto, teoremas libres: sabes qué 'id :: a -> a' o' fst :: (a, b) -> a' hacen simplemente por sus firmas de tipo, porque solo tienen polimorfismo paramétrico. Si tiene algún tipo de mecanismo de tipo de letra, entonces pierde la parametricidad. Por ejemplo, 'a -> a -> Bool' admite solo dos implementaciones (totales) en Haskell (' const $ const True' y 'const $ const False'); si tiene typecase, entonces pierde esta garantía. –

+0

@ AntalS-Z: Solo me aseguro de que te entiendo correctamente. ¿Estás diciendo que puedo declarar 'id :: a -> a', luego definir' id (Int i) = i + 1'? Porque ese no es el caso: la última definición sería del tipo 'SupportsPlus a => a -> a', que es incompatible. –

+5

Eso es lo que parece de su pregunta; su firma de tipo para '(==)' se da como 'a -> a -> Bool'. Si tiene que especificar una "clase de tipo" 'SupportsEquals', entonces parece que todo lo que ha hecho es sintaxis comercializada --- no veo por qué es * diferente *. Y no creo que pueda expresar clases de tipos de parámetros múltiples, dependencias funcionales o tipos asociados, aunque estos ciertamente no estaban en el plan original. De hecho, ¿puede incluso expresar clases de tipo de mayor nivel? ¿Cómo sabrías que 'return :: a -> m a' debería ser ad-hoc sobre' m', y no 'a'? –

Respuesta

12

¿Este mecanismo de polimorfismo ad-hoc es demasiado ad-hoc?

Esta pregunta simplemente mendiga para estar vinculada al documento de 1988 de Philip Wadler y Steve Blott, How to make ad-hoc polymorphism less ad-hoc, donde presentan la idea de las clases de tipo. Wadler es probablemente "palabra de Dios" en este caso.

Hay algunos problemas que veo con la propuesta de "coincidencia de patrones en cualquier tipo de datos Haskell".

La técnica de coincidencia de patrones es no suficiente para definir las constantes polimórficas, como mempty :: Monoid a => a.

La técnica de coincidencia de patrones aún recae en las clases de tipos, excepto de una manera peor. Escriba las clases clasifique los tipos (vea la figura). Pero la técnica de combinación de patrones lo hace bastante vago. ¿Cómo se supone que debe especificar que las funciones foo y bar son parte de la "misma" clase? Las restricciones de clase de tipo serían completamente ilegibles si tiene que agregar una nueva para cada función polimórfica que use.

La técnica de coincidencia de patrones introduce una nueva sintaxis en Haskell, que complica la especificación de idioma. La palabra clave default no se ve tan mal, pero la coincidencia de patrones "en tipos" es nueva y confusa.

La coincidencia de patrones "en tipos de datos comunes" derrota el estilo sin puntos. En lugar de (==) = intEq, tenemos (Int a) == (Int b) = intEq a b; este tipo de coincidencia de patrón artificial evita la reducción de eta.

Finalmente, cambia por completo nuestra comprensión de las firmas de tipo.a -> a -> Foo es actualmente una garantía que las entradas no se pueden inspeccionar. No se puede asumir nada sobre las entradas a, excepto que las dos entradas son del mismo tipo.[a] -> [a] otra vez significa que los elementos de la lista no pueden ser inspeccionados de ninguna manera significativa, dándole Theorems for Free (otro documento de Wadler).

Puede haber formas de abordar estas preocupaciones, pero mi impresión general es que las clases de tipo ya resuelven este problema de una manera elegante, y la técnica de coincidencia de patrones sugerida no agrega ningún beneficio, mientras causa varios problemas.

+1

wrt. su último punto, no creo que su sugerencia sea cambiar el polimorfismo por completo, creo que es solo agregar un montón de restricciones de tipo para cada función utilizada. es decir, 'a -> a -> Foo' aún garantizaría que las entradas no puedan inspeccionarse; si se usa una clase de tipo implícita, eso se convierte en parte de la firma. – gatoatigrado

+0

Muchas gracias por la información y el documento. No estoy de acuerdo con un par de tus puntos, sin embargo.Las restricciones de clase de tipo no se volverían ilegibles, porque ni siquiera necesitarían estar visibles; simplemente podría decir "' (+) no definido en (foo :: Foo + bar :: Bar) '". El punto acerca de las firmas de tipo es importante. Usted sabe a ciencia cierta que 'id :: a -> a' todavía, porque una definición que involucra cualquier otra función tendría una firma incompatible. Además, re. eta-reduction, estás escribiendo en un lenguaje aplicativo (en lugar de concatenado), así que no todo puede estar libre de puntos de todos modos. –

+0

@gatoatigrado: Sí, eso es exactamente lo que quiero decir. No rompe ninguno de los teoremas que ya tenemos. Al menos yo no lo creo –

4

No conozco la Palabra de Dios, pero aquí hay algunos argumentos.

Ya no está definiendo una función en el mismo módulo único. Ahora puede escribir

(==) = internalIntEq 
(==) = internalFloatEq 

Esto hace que el código sea menos legible. Hay una propuesta llamada "TypeBasedNameResolution" que hace algo similar, pero el hecho importante es que tal tipo de bifurcación solo se hace para (==) de diferentes módulos.

Es una mala práctica que el compilador agregue identificadores. En tu caso, creas automáticamente la clase de tipo SupportsEqualsEquals. Un nuevo usuario podría preguntarse, "de dónde viene esto", y no habría una fuente correspondiente que lo defina.

Saltarse las firmas de instancia de escritura no le da tanto como podría pensar. Puede obtener los parámetros necesarios a través, p. :t internalIntEq en ghci. Supongo que podría ser más conveniente, pero prefiero tener una herramienta que podría preguntar "¿cuál es el tipo de instancia para Eq donde == es internalIntEq".

Las características de clase más avanzadas no están claras.¿Dónde colocas los tipos asociados y las dependencias funcionales? ¡Estos son realmente importantes para mí!

Su valor por defecto hace que la compilación modular sea más difícil. No vas a obtener clases extensibles de forma gratuita. Consideremos,

f :: Supports[==] a => a -> a -> Bool 
f = (/=) 

lo que tengo entendido, esta compila abajo en

f :: Instance (Supports[==]) a -> a -> a -> Bool 
f eq_inst x y = not (x eq_inst.== y) 

Ahora, si proporciono un nuevo /= ejemplo, para un tipo particular a_0 y alimentar a algunos x :: a_0 en f, entonces

f x x = not (x == x) 
-- computation you really want: f x x = x /= x, using the new /= instance for a_0 

Puede preguntar, "cuando uno sería tan tonto para restringir f a Supports[==] en lugar de Supports[/=]? " Pero los contextos pueden provenir de más que firmas de funciones; pueden provenir de funciones de orden superior, etc., etc.

Espero que ayude.

Cuestiones relacionadas