El comprobador de tipos está permitiendo un reemplazo de tipos muy incorrecto, y el programa aún compila


Al tratar de depurar un problema en mi programa (2 círculos con un radio igual están siendo dibujados a diferentes tamaños usando Brillo), me topé con una situación extraña. En mi archivo que maneja objetos, tengo la siguiente definición para un Player:

type Coord = (Float,Float)
data Obj =  Player  { oPos :: Coord, oDims :: Coord }

Y en mi archivo principal, que importa Objetos.hs, tengo la siguiente definición:

startPlayer :: Obj
startPlayer = Player (0,0) 10

Esto sucedió debido a que agregué y cambié los campos para el jugador, y me olvidé de actualizar startPlayer después (sus dimensiones fueron determinado por un solo número para representar un radio, pero lo cambié a un Coord para representar (ancho,alto); en caso de que alguna vez haga que el objeto del jugador no sea un círculo).

Lo sorprendente es que el código anterior compila y se ejecuta, a pesar de que el segundo campo es del tipo incorrecto.

Primero pensé que tal vez tenía diferentes versiones de los archivos abiertos, pero cualquier cambio en cualquier archivo se reflejó en el programa compilado.

Luego pensé que tal vez startPlayer no estaba siendo utilizado por alguna razón. Sin embargo, comentar startPlayer produce un error de compilador, y aún más extraño, cambiar el 10 en startPlayer causa una respuesta apropiada (cambia el tamaño inicial del Player); de nuevo, a pesar de ser del tipo incorrecto. Para asegurarme de que está leyendo la definición de datos correctamente, inserté un error tipográfico en el archivo, y me dio un error; así que estoy mirando el archivo correcto.

Intenté pegar los 2 fragmentos anteriores en su propio archivo, y escupió el error esperado que el segundo campo de Player en startPlayer es incorrecto.

¿Qué podría permitir que esto suceda? Uno pensaría que esto es lo que el comprobador de tipos de Haskell debería evitar.


Debo señalar que la respuesta a mi problema original, dos círculos de radio supuestamente igual siendo dibujados a diferentes tamaños, fue que uno de los radios era en realidad negativo.

Author: Carcigenicate, 2014-11-06

2 answers

La única forma en que esto podría compilarse es si existe una instancia Num (Float,Float). Esto no es proporcionado por la biblioteca estándar, aunque es posible que una de las bibliotecas que está utilizando lo agregó por alguna razón loca. Intente cargar su proyecto en ghci y vea si 10 :: (Float,Float) funciona, luego intente :i Num para averiguar de dónde viene la instancia y luego grite a quien la definió.

Anexo: No hay forma de desactivar las instancias. Ni siquiera hay una manera de no exportarlos desde un módulo. Si esto fuera posible, llevaría incluso a más código confuso. La única solución real aquí es no definir instancias como esa.

 128
Author: Cubic,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-11-06 02:10:11

El comprobador de tipos de Haskell está siendo razonable. El problema es que los autores de una biblioteca que estás usando han hecho algo... menos razonable.

La respuesta breve es: Sí, 10 :: (Float, Float) es perfectamente válida si hay una instancia Num (Float, Float). No hay nada "muy malo" en ello desde la perspectiva del compilador o del lenguaje. Simplemente no encaja con nuestra intuición sobre lo que hacen los literales numéricos. Ya estás acostumbrado al tipo de sistema captura el tipo de error que ha cometido, está justificadamente sorprendido y decepcionado!

Num instancias y el problema fromInteger

Te sorprende que el compilador acepte 10 :: Coord, es decir, 10 :: (Float, Float). Es razonable suponer que literales numéricos como 10 se inferirán que tienen tipos "numéricos". Fuera de la caja, los literales numéricos se pueden interpretar como Int, Integer, Float, o Double. Una tupla de números, sin otro contexto, no parece un número en la forma en que esos cuatro tipos son números. No estamos hablando de Complex.

Afortunadamente o desafortunadamente, Haskell es un lenguaje muy flexible. El estándar especifica que un literal entero como 10 se interpretará como fromInteger 10, que tiene el tipo Num a => a. Así que 10 podría inferirse como cualquier tipo que tenga una instancia Num escrita para él. Explico esto con un poco más de detalle en otra respuesta .

Así que cuando publicaste tu pregunta, un Haskeller experimentado inmediatamente la detectó para 10 :: (Float, Float) ser aceptado, debe haber una instancia como Num a => Num (a, a) o Num (Float, Float). No hay tal instancia en el Prelude, por lo que debe haber sido definida en otro lugar. Usando :i Num, rápidamente vio de dónde venía: el paquete gloss.

Sinónimos de tipo e instancias huérfanas

Pero, espera un minuto. No está usando ningún tipo gloss en este ejemplo; ¿por qué le afectó la instancia de gloss? La respuesta viene en dos pasos.

Primero, un sinónimo de tipo introducido con la palabra clave type no crea un nuevo tipo. En tu módulo, escribir Coord es simplemente una abreviatura de (Float, Float). Del mismo modo en Graphics.Gloss.Data.Point, Point significa (Float, Float). En otras palabras, sus Coord y gloss's Point son literalmente equivalentes.

Así que cuando los mantenedores gloss eligieron escribir instance Num Point where ..., también hicieron que su Coord escriba una instancia de Num. Eso es equivalente a instance Num (Float, Float) where ... o instance Num Coord where ....

(Por defecto, Haskell no permite que los sinónimos de tipo sean instancias de clase. Los autores gloss tuvo que habilitar un par de extensiones de lenguaje, TypeSynonymInstances y FlexibleInstances, para escribir la instancia.)

Segundo, esto es sorprendente porque es una instancia huérfana, es decir, una declaración de instancia instance C A donde tanto C como A están definidos en otros módulos. Aquí es particularmente insidioso porque cada parte involucrada, i. e.Num, (,), y Float, viene del Prelude y es probable que esté en alcance en todas partes.

Su expectativa es que Num se define en Prelude, y las tuplas y Float se definen en Prelude, por lo que todo sobre cómo funcionan esas tres cosas se define en Prelude. ¿Por qué la importación de un módulo completamente diferente cambiaría algo? Idealmente no, pero las instancias huérfanas rompen esa intuición.

(Nótese que GHC advierte sobre las instancias huérfanas-los autores de gloss anularon específicamente esa advertencia. Eso debería haber levantado una bandera roja y provocado al menos una advertencia en la documentación.)

Las instancias de clase son globales y no pueden estar oculto

Además, las instancias de clase son globales: cualquier instancia definida en cualquier módulo que se importe transitivamente desde su módulo estará en contexto y disponible para el typechecker al hacer la resolución de instancia. Esto hace que el razonamiento global sea conveniente, porque podemos (generalmente) asumir que una función de clase como (+) siempre será la misma para un tipo dado. Sin embargo, también significa que las decisiones locales tienen efectos globales; definir una instancia de clase cambia irrevocablemente el contexto del código descendente, sin forma de enmascararlo u ocultarlo detrás de los límites del módulo.

Usted no puede usar listas de importación para evitar importar instancias. Del mismo modo, no puede evitar exportar instancias de los módulos que defina.

Este es un área problemática y muy discutida del diseño del lenguaje Haskell. Hay una fascinante discusión de temas relacionados en este hilo de reddit. Véase, por ejemplo, el comentario de Edward Kmett sobre permitir el control de la visibilidad por ejemplo: "Básicamente se descarta la corrección de casi todo el código que he escrito."

(Por cierto, como esta respuesta demostró , usted puede romper la suposición de instancia global en algunos aspectos mediante el uso de instancias huérfanas!)

Qué hacer-para implementadores de bibliotecas

Piénsalo dos veces antes de implementar Num. No se puede solucionar el problema de fromInteger-no, definir fromInteger = error "not implemented" hace que no lo haga mejor. ¿Sus usuarios estarán confundidos o sorprendidos, o peor aún, nunca se darán cuenta, si sus literales enteros se infieren accidentalmente para tener el tipo que está instanciando? ¿Proporcionar (*) y (+) es crítico, particularmente si tiene que hackearlo?

Considere usar operadores aritméticos alternativos definidos en una biblioteca como la de Conal Elliott vector-space (para tipos de tipo *) o de Edward Kmett linear (para los tipos de clase * -> *). Esto es lo que tiendo a hacer yo mismo.

Use -Wall. No implemente instancias huérfanas y no deshabilite la advertencia de instancia huérfana.

Alternativamente, siga el ejemplo de linear y muchas otras bibliotecas de buen comportamiento, y proporcione instancias huérfanas en un módulo separado que termine en .OrphanInstances o .Instances. Y no importe ese módulo desde ningún otro módulo. A continuación, los usuarios pueden importar los huérfanos explícitamente si lo desean.

Si te encuentras definiendo huérfanos, considera preguntar desarrolladores de upstream para implementarlos en su lugar, si es posible y apropiado. Solía escribir con frecuencia la instancia huérfana Show a => Show (Identity a), hasta que la agregaron a transformers. Puede que incluso haya presentado un informe de error al respecto; no lo recuerdo.

Qué hacer-para los consumidores de bibliotecas

No tienes muchas opciones. Llegar-cortésmente y constructivamente!- a los encargados de la biblioteca. Indíqueles esta pregunta. Pueden haber tenido alguna razón especial para escribir el huérfano problemático, o simplemente no me doy cuenta.

En términos más generales: Sea consciente de esta posibilidad. Esta es una de las pocas áreas de Haskell donde hay verdaderos efectos globales; tendrías que comprobar que cada módulo que importas, y cada módulo esos módulos importados, no implementa instancias huérfanas. Las anotaciones de tipo a veces pueden alertarle de problemas, y por supuesto puede usar :i en GHCi para verificar.

Defina sus propios newtype s en lugar de type sinónimos si es lo suficientemente importante. Puedes ser bonita seguro que nadie se meterá con ellos.

Si tiene problemas frecuentes derivados de una biblioteca de código abierto, por supuesto, puede hacer su propia versión de la biblioteca, pero el mantenimiento puede convertirse rápidamente en un dolor de cabeza.

 61
Author: Christian Conkle,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:34:28