Instancias huérfanas en Haskell

86

Al compilar mi aplicación Haskell con la -Wallopción, GHC se queja de instancias huérfanas, por ejemplo:

Publisher.hs:45:9:
    Warning: orphan instance: instance ToSElem Result

La clase de tipo ToSElemno es mía, está definida por HStringTemplate .

Ahora sé cómo solucionar esto (mueva la declaración de la instancia al módulo donde se declara Result), y sé por qué GHC preferiría evitar las instancias huérfanas , pero sigo creyendo que mi camino es mejor. No me importa si el compilador tiene inconvenientes, más que yo.

La razón por la que quiero declarar mis ToSEleminstancias en el módulo Publisher es porque es el módulo Publisher el que depende de HStringTemplate, no de los otros módulos. Estoy tratando de mantener una separación de preocupaciones y evitar que cada módulo dependa de HStringTemplate.

Pensé que una de las ventajas de las clases de tipos de Haskell, en comparación, por ejemplo, con las interfaces de Java, es que son abiertas en lugar de cerradas y, por lo tanto, las instancias no tienen que declararse en el mismo lugar que el tipo de datos. El consejo de GHC parece ser ignorar esto.

Entonces, lo que estoy buscando es alguna validación de que mi pensamiento es sólido y que estaría justificado ignorar / suprimir esta advertencia, o un argumento más convincente en contra de hacer las cosas a mi manera.

Dan Dyer
fuente
La discusión en las respuestas y comentarios ilustra que hay una gran diferencia entre definir instancias huérfanas en un ejecutable , como lo está haciendo, y en una biblioteca que está expuesta a otros. Esta pregunta tremendamente popular ilustra lo confusas que pueden ser las instancias huérfanas para los usuarios finales de una biblioteca que las define.
Christian Conkle

Respuestas:

94

Entiendo por qué quiere hacer esto, pero desafortunadamente, puede ser solo una ilusión que las clases de Haskell parecen estar "abiertas" en la forma en que usted dice. Mucha gente siente que la posibilidad de hacer esto es un error en la especificación de Haskell, por las razones que explicaré a continuación. De todos modos, si realmente no es apropiado para la instancia, debe declararse en el módulo donde se declara la clase o en el módulo donde se declara el tipo, eso probablemente sea una señal de que debería estar usando una newtypeo alguna otra envoltura alrededor de su tipo.

Las razones por las que deben evitarse las instancias huérfanas son mucho más profundas que la conveniencia del compilador. Este tema es bastante controvertido, como puede ver en otras respuestas. Para equilibrar la discusión, voy a explicar el punto de vista de que uno nunca, nunca, debe escribir instancias huérfanas, que creo que es la opinión mayoritaria entre los Haskellers experimentados. Mi propia opinión está en algún punto intermedio, que explicaré al final.

El problema surge del hecho de que cuando existe más de una declaración de instancia para la misma clase y tipo, no hay ningún mecanismo en Haskell estándar para especificar cuál usar. Más bien, el programa es rechazado por el compilador.

El efecto más simple de eso es que podría tener un programa que funcione perfectamente y que de repente deje de compilarse debido a un cambio que alguien más hace en alguna dependencia lejana de su módulo.

Peor aún, es posible que un programa en funcionamiento comience a fallar en tiempo de ejecución debido a un cambio distante. Podría estar usando un método que asume que proviene de una determinada declaración de instancia, y podría ser reemplazado silenciosamente por una instancia diferente que sea lo suficientemente diferente como para hacer que su programa comience a fallar inexplicablemente.

Las personas que quieren garantías de que estos problemas nunca les sucederán deben seguir la regla de que si alguien, en cualquier lugar, alguna vez ha declarado una instancia de cierta clase para un cierto tipo, ninguna otra instancia debe volver a declararse en ningún programa escrito. Por cualquiera. Por supuesto, existe la solución de usar a newtypepara declarar una nueva instancia, pero eso siempre es al menos un inconveniente menor y, a veces, uno importante. Entonces, en este sentido, aquellos que escriben instancias huérfanas intencionalmente están siendo bastante descorteses.

Entonces, ¿qué se debe hacer con este problema? El campo anti-instancia huérfana dice que la advertencia de GHC es un error, debe ser un error que rechace cualquier intento de declarar una instancia huérfana. Mientras tanto, debemos ejercitar la autodisciplina y evitarlos a toda costa.

Como has visto, hay quienes no están tan preocupados por esos posibles problemas. De hecho, fomentan el uso de instancias huérfanas como una herramienta para la separación de preocupaciones, como usted sugiere, y dicen que uno debe asegurarse, caso por caso, de que no haya ningún problema. Las instancias huérfanas de otras personas me han molestado suficientes veces para convencerme de que esta actitud es demasiado arrogante.

Creo que la solución correcta sería agregar una extensión al mecanismo de importación de Haskell que controlaría la importación de instancias. Eso no resolvería los problemas por completo, pero ayudaría en cierta medida a proteger nuestros programas contra el daño de las instancias huérfanas que ya existen en el mundo. Y luego, con el tiempo, podría convencerme de que, en ciertos casos limitados, quizás una instancia huérfana no sea tan mala. (Y esa misma tentación es la razón por la que algunos en el campo anti-instancia huérfana se oponen a mi propuesta).

Mi conclusión de todo esto es que, al menos por el momento, le recomendaría encarecidamente que evite declarar cualquier instancia huérfana, para ser considerado con los demás si no es por otra razón. Utilice un newtype.

Yitz
fuente
4
En particular, esto es cada vez más un problema con el crecimiento de las bibliotecas. Con más de 2200 bibliotecas en Haskell y decenas de miles de módulos individuales, el riesgo de captar instancias aumenta drásticamente.
Don Stewart
16
Re: "Creo que la solución correcta sería agregar una extensión al mecanismo de importación de Haskell que controlaría la importación de instancias" En caso de que esta idea le interese a alguien, podría valer la pena mirar el lenguaje Scala como ejemplo; tiene características muy parecidas a esta para controlar el alcance de los 'implícitos', que se pueden usar de manera muy similar a las instancias de clases de tipos.
Matt
5
Mi software es una aplicación en lugar de una biblioteca, por lo que la posibilidad de causar problemas a otros desarrolladores es prácticamente nula. Se podría considerar el módulo Publisher como la aplicación y el resto de módulos como una biblioteca, pero si distribuyera la biblioteca sería sin el Publisher y, por tanto, las instancias huérfanas. Pero si moviera las instancias a los otros módulos, la biblioteca se enviaría con una dependencia innecesaria de HStringTemplate. Entonces, en este caso, creo que los huérfanos están bien, pero prestaré atención a sus consejos si encuentro el mismo problema en un contexto diferente.
Dan Dyer
1
Suena como un enfoque razonable. Lo único a tener en cuenta es si el autor de un módulo que importa agrega esta instancia en una versión posterior. Si esa instancia es la misma que la suya, deberá eliminar su propia declaración de instancia. Si esa instancia es diferente a la suya, deberá colocar un contenedor de tipo nuevo alrededor de su tipo, lo que podría ser una refactorización significativa de su código.
Yitz
@Matt: de hecho, ¡sorprendentemente Scala hace esto justo donde Haskell no! (excepto, por supuesto, Scala carece de sintaxis de primera clase para la maquinaria de clase de tipo, que es aún peor ...)
Erik Kaplun
44

¡Adelante, suprime esta advertencia!

Estás en buena compañía. Conal lo hace en "TypeCompose". "chp-mtl" y "chp-transformers" lo hacen, "control-monad-exception-mtl" y "control-monad-exception-monadsfd" lo hacen, etc.

por cierto, probablemente ya lo sepas, pero para aquellos que no lo hacen y tropiezan con tu pregunta en una búsqueda:

{-# OPTIONS_GHC -fno-warn-orphans #-}

Editar:

Reconozco los problemas que Yitz mencionó en su respuesta como problemas reales. Sin embargo, no veo el uso de instancias huérfanas como un problema también, y trato de elegir el "menor de todos los males", que es en mi humilde opinión utilizar prudentemente instancias huérfanas.

Solo utilicé un signo de exclamación en mi respuesta corta porque su pregunta muestra que ya conoce bien los problemas. De lo contrario, habría sido menos entusiasta :)

Un poco de diversión, pero lo que creo que es la solución perfecta en un mundo perfecto sin compromiso:

Creo que los problemas que menciona Yitz (sin saber qué instancia se elige) podrían resolverse en un sistema de programación "holístico" donde:

  • No está editando meros archivos de texto de forma primitiva, sino que cuenta con la ayuda del entorno (por ejemplo, la finalización del código solo sugiere cosas de tipos relevantes, etc.)
  • El lenguaje de "nivel inferior" no tiene soporte especial para clases de tipos, y en su lugar, las tablas de funciones se pasan explícitamente
  • Pero, el entorno de programación de "nivel superior" muestra el código de manera similar a cómo se presenta Haskell ahora (normalmente no verá las tablas de funciones transmitidas), y elige las clases de tipos explícitas para usted cuando son obvias (por ejemplo, todos los casos de Functor tienen solo una opción) y cuando hay varios ejemplos (lista comprimida Aplicativa o lista-mónada Aplicativa, Primero / Último / elevación tal vez Monoide) te permite elegir qué instancia usar.
  • En cualquier caso, incluso cuando la instancia se eligió automáticamente, el entorno le permite ver fácilmente qué instancia se usó, con una interfaz sencilla (un hipervínculo o una interfaz flotante o algo así)

De vuelta del mundo de fantasía (o, con suerte, del futuro), ahora mismo: recomiendo tratar de evitar las instancias huérfanas mientras las sigues usando cuando "realmente necesitas"

yairchu
fuente
5
Sí, pero podría decirse que cada una de esas ocurrencias es un error de algún orden. Me vienen a la mente las malas instancias en control-monad-exception-mtl y monads-fd para Either. Sería menos molesto si cada uno de esos módulos se viera obligado a definir sus propios tipos o suministrar envoltorios de nuevos tipos. Casi todas las instancias huérfanas son un dolor de cabeza esperando a que suceda, y si nada más, requerirá su vigilancia constante para asegurarse de que se importe o no sea apropiado.
Edward KMETT
2
Gracias. Creo que los usaré en esta situación particular, pero gracias a Yitz ahora tengo una mejor apreciación de los problemas que pueden causar.
Dan Dyer
37

Las instancias huérfanas son una molestia, pero en mi opinión, a veces son necesarias. A menudo combino bibliotecas donde un tipo proviene de una biblioteca y una clase proviene de otra biblioteca. Por supuesto, no se puede esperar que los autores de estas bibliotecas proporcionen instancias para cada combinación concebible de tipos y clases. Así que tengo que proporcionarlos y son huérfanos.

La idea de que debe envolver el tipo en un nuevo tipo cuando necesita proporcionar una instancia es una idea con mérito teórico, pero es demasiado tediosa en muchas circunstancias; es el tipo de idea que presentan las personas que no se ganan la vida escribiendo código Haskell. :)

Así que adelante, proporcione instancias huérfanas. Son inofensivos.
Si puede bloquear ghc con instancias huérfanas, entonces eso es un error y debe informarse como tal. (El error que ghc tenía / tiene acerca de no detectar múltiples instancias no es tan difícil de solucionar).

Pero tenga en cuenta que en algún momento en el futuro alguien más podría agregar la instancia some como ya lo ha hecho, y podría obtener un error (tiempo de compilación).

augusts
fuente
2
Un buen ejemplo es el (Ord k, Arbitrary k, Arbitrary v) ⇒ Arbitrary (Map k v)uso de QuickCheck.
Erik Kaplun
17

En este caso, creo que el uso de instancias huérfanas está bien. La regla general para mí es: puede definir una instancia si "posee" la clase de tipos o si "posee" el tipo de datos (o algún componente del mismo, es decir, una instancia para Maybe MyData también está bien, al menos a veces). Dentro de esas restricciones, el lugar donde decida colocar la instancia es asunto suyo.

Hay una excepción más: si no posee la clase de tipos ni el tipo de datos, pero está produciendo un binario y no una biblioteca, también está bien.

sclv
fuente
5

(Sé que llego tarde a la fiesta, pero esto puede ser útil para otros)

Puede mantener las instancias huérfanas en su propio módulo, entonces si alguien importa ese módulo es específicamente porque lo necesita y puede evitar importarlo si causa problemas.

Trystan Spangler
fuente
3

En este sentido, entiendo la posición de las bibliotecas WRT del campo de instancias anti-huérfanas, pero para los objetivos ejecutables, ¿no deberían estar bien las instancias huérfanas?

mxc
fuente
3
En términos de ser descortés con los demás, tienes razón. Pero se está abriendo a posibles problemas futuros si la misma instancia se define alguna vez en el futuro en algún lugar de su cadena de dependencia. Entonces, en este caso, depende de usted decidir si vale la pena correr el riesgo.
Yitz
5
En casi todos los casos de implementación de una instancia huérfana en un ejecutable, es para llenar un vacío que desea que ya esté definido. Entonces, si la instancia aparece en sentido ascendente, el error de compilación resultante es solo una señal útil para indicarle que puede eliminar su declaración de la instancia.
Ben