Mientras intentaba depurar un problema en mi programa (se dibujan 2 círculos con un radio igual en diferentes tamaños usando Gloss *
), me encontré con una situación extraña. En mi archivo que maneja objetos, tengo la siguiente definición para unPlayer
:
type Coord = (Float,Float)
data Obj = Player { oPos :: Coord, oDims :: Coord }
y en mi archivo principal, que importa Objects.hs, tengo la siguiente definición:
startPlayer :: Obj
startPlayer = Player (0,0) 10
Esto sucedió debido a que agregué y cambié campos para el jugador, y me olvidé de actualizar startPlayer
después (sus dimensiones fueron determinadas por un solo número para representar un radio, pero lo cambié a Coord
para representar (ancho, alto); en caso de que alguna vez haga el jugador objeta un no-círculo).
Lo sorprendente es que el código anterior se 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 los archivos se reflejaba en el programa compilado.
Luego pensé que tal vez startPlayer
no se estaba usando por alguna razón. Comentando startPlayer
produce un error de compilación sin embargo, y aún más extraño, cambiando el 10
en startPlayer
causas una respuesta apropiada (cambia el tamaño de partida de la 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; entonces estoy mirando el archivo correcto.
Intenté pegar los 2 fragmentos de arriba en su propio archivo, y escupió el error esperado de que el segundo campo de Player
en startPlayer
es incorrecta.
¿Qué podría permitir que esto sucediera? Uno pensaría que esto es exactamente lo que el verificador de tipos de Haskell debería prevenir.
*
La respuesta a mi problema original, dos círculos de radio supuestamente iguales dibujados en diferentes tamaños, fue que uno de los radios era en realidad negativo.
Point
anewtype
o usar otros nombres de operador alalinear
)Respuestas:
La única forma en que esto podría compilarse es si existe una
Num (Float,Float)
instancia. Esto no lo proporciona la biblioteca estándar, aunque es posible que una de las bibliotecas que está utilizando lo haya agregado por alguna razón loca. Intente cargar su proyecto en ghci y vea si10 :: (Float,Float)
funciona, luego intente:i Num
averiguar de dónde proviene la instancia y luego grite a quien la definió.Anexo: no hay forma de desactivar las instancias. Ni siquiera hay una forma de no exportarlos desde un módulo. Si esto fuera posible, conduciría a un código aún más confuso. La única solución real aquí es no definir instancias como esa.
fuente
10 :: (Float, Float)
produce(10.0,10.0)
y:i Num
contiene la líneainstance Num Point -- Defined in ‘Graphics.Gloss.Data.Point’
(Point
es el alias de Coord de Gloss). ¿Seriamente? Gracias. Eso me salvó de una noche de insomnio.Num
dónde tiene sentido, como unAngle
tipo de datos que restringe unDouble
entre-pi
ypi
, o si alguien quisiera escribir un tipo de datos representando cuaterniones o algún otro tipo numérico más complejo, esta característica es muy conveniente. También sigue las mismas reglas queString
/Text
/ByteString
, permitir estas instancias tiene sentido desde el punto de vista de la facilidad de uso, pero puede ser mal utilizado como en este caso.newtype
declaración para enCoord
lugar de unatype
.Num (Float, Float)
o incluso(Floating a) => Num (a,a)
no requeriría la extensión pero daría como resultado el mismo comportamiento.El verificador 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álido si hay una instanciaNum (Float, Float)
. No tiene nada de "muy malo" desde la perspectiva del compilador o del lenguaje. Simplemente no cuadra con nuestra intuición sobre lo que hacen los literales numéricos. Ya que está acostumbrado a que el sistema de tipos detecte el tipo de error que cometió, ¡está sorprendido y decepcionado con razón!Num
instancias y elfromInteger
problemaLe sorprende que el compilador acepte
10 :: Coord
, es decir10 :: (Float, Float)
. Es razonable suponer que se inferirá que los literales numéricos como10
tienen tipos "numéricos". Fuera de la caja, literales numéricos se pueden interpretar comoInt
,Integer
,Float
, oDouble
. 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 deComplex
.Sin embargo, por suerte o por desgracia, Haskell es un idioma muy flexible. El estándar especifica que un literal entero como
10
se interpretará comofromInteger 10
, que tiene tipoNum a => a
. Por10
lo tanto, podría inferirse como cualquier tipo que tenga unaNum
instancia escrita para él. Explico esto con un poco más de detalle en otra respuesta .Entonces, cuando publicó su pregunta, un Haskeller experimentado inmediatamente notó que para
10 :: (Float, Float)
ser aceptado, debe haber una instancia comoNum a => Num (a, a)
oNum (Float, Float)
. No existe tal instancia en elPrelude
, por lo que debe haberse definido en otro lugar. Usando:i Num
, rápidamente descubrió de dónde venía: elgloss
paquete.Sinónimos de tipo e instancias huérfanas
Pero espera un minuto. No está utilizando ningún
gloss
tipo en este ejemplo; ¿Por quégloss
te afectó la instancia en ? La respuesta viene en dos pasos.Primero, un sinónimo de tipo introducido con la palabra clave
type
no crea un nuevo tipo . En su módulo, escribirCoord
es simplemente una abreviatura de(Float, Float)
. Del mismo modo enGraphics.Gloss.Data.Point
,Point
significa(Float, Float)
. En otras palabras, losCoord
ygloss
'sPoint
son, literalmente equivalente.Entonces, cuando los
gloss
mantenedores decidieron escribirinstance Num Point where ...
, también hicieron de suCoord
tipo una instancia deNum
. Eso es equivalente ainstance Num (Float, Float) where ...
oinstance Num Coord where ...
.(De forma predeterminada, Haskell no permite que los sinónimos de tipo sean instancias de clase. Los
gloss
autores tuvieron que habilitar un par de extensiones de idiomaTypeSynonymInstances
yFlexibleInstances
, para escribir la instancia).En segundo lugar, esto es sorprendente porque es una instancia huérfana , es decir, una declaración de instancia
instance C A
donde ambosC
yA
están definidos en otros módulos. Aquí es particularmente insidioso porque cada parte involucrada, es decirNum
,(,)
yFloat
, proviene delPrelude
y es probable que esté dentro del alcance en todas partes.Su expectativa es que
Num
se define enPrelude
, y las tuplas yFloat
se definen enPrelude
, por lo que todo sobre cómo funcionan esas tres cosas se define enPrelude
. ¿Por qué importar un módulo completamente diferente cambiaría algo? Idealmente no lo haría, pero las instancias huérfanas rompen esa intuición.(Tenga en cuenta que GHC advierte sobre instancias huérfanas; los autores de
gloss
específicamente anularon esa advertencia. Eso debería haber levantado una bandera roja y haber provocado al menos una advertencia en la documentación).Las instancias de clase son globales y no se pueden ocultar
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 verificador de tipos cuando realice la resolución de instancia. Esto hace que el razonamiento global sea conveniente, porque podemos (normalmente) 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; la definición de 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.No puede utilizar listas de importación para evitar importar instancias . Del mismo modo, no puede evitar exportar instancias de los módulos que defina.
Esta es un área problemática y muy discutida del diseño del lenguaje Haskell. Hay una discusión fascinante sobre temas relacionados en este hilo de reddit . Vea, por ejemplo, el comentario de Edward Kmett sobre permitir el control de visibilidad para casos: "Básicamente, descarta la exactitud de casi todo el código que he escrito".
(Por cierto, como demostró esta respuesta , ¡ puede romper la suposición de instancia global en algunos aspectos utilizando instancias huérfanas!)
Qué hacer: para los implementadores de bibliotecas
Piense dos veces antes de implementar
Num
. No se puede evitar elfromInteger
problema, no, la definiciónfromInteger = error "not implemented"
no no hacerlo mejor. ¿Se confundirán o sorprenderán sus usuarios, o peor aún, nunca se darán cuenta, si se infiere accidentalmente que sus literales enteros tienen el tipo que está instanciando? ¿Proporcionar(*)
y(+)
eso es fundamental, especialmente si tienes que piratearlo?Considere utilizar operadores aritméticos alternativos definidos en una biblioteca como la de Conal Elliott
vector-space
(para tipos de género*
) o la de Edward Kmettlinear
(para tipos de género* -> *
). Esto es lo que suelo hacer yo mismo.Utilice
-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 con 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 . Luego, los usuarios pueden importar los huérfanos explícitamente si lo desean.Si se encuentra definiendo a los huérfanos, considere pedirles a los mantenedores ascendentes que los implementen 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 atransformers
. Puede que incluso haya presentado un informe de error al respecto; No recuerdoQué hacer para los consumidores de bibliotecas
No tienes muchas opciones. Comuníquese, de manera educada y constructiva, con los encargados de la biblioteca. Indíqueles esta pregunta. Es posible que hayan tenido alguna razón especial para escribir sobre el huérfano problemático, o es posible que simplemente no se den cuenta.
En términos más generales: tenga en cuenta esta posibilidad. Esta es una de las pocas áreas de Haskell donde hay verdaderos efectos globales; tendría que verificar que cada módulo que importe, y cada módulo que importen esos módulos, no implemente instancias huérfanas. Las anotaciones de tipo a veces pueden alertarlo sobre problemas y, por supuesto, puede usarlas
:i
en GHCi para verificar.Defina sus propios
newtype
s en lugar detype
sinónimos si es lo suficientemente importante. Puede estar bastante seguro de que nadie se meterá con ellos.Si tiene problemas frecuentes derivados de una biblioteca de código abierto, por supuesto, puede crear su propia versión de la biblioteca, pero el mantenimiento puede convertirse rápidamente en un dolor de cabeza.
fuente