haskell - tipos - tabla de casos de uso
¿Hay buenos casos de uso para OverlappingInstances? (3)
La mayoría de las personas solicitan instancias superpuestas porque desean una inferencia dirigida por restricciones en lugar de una inferencia dirigida por tipo. Las clases de tipo se crearon para la inferencia dirigida por el tipo y Haskell no proporciona una solución elegante para la inferencia dirigida por la restricción.
Sin embargo, aún puede "encapsular la bondad" usando newtypes. Dada la siguiente definición de instancia que es propensa a la superposición de instancias:
instance (SomeConstraint a) => SomeClass a where ...
En su lugar puedes usar:
newtype N a = N { unN :: a }
instance (SomeConstraint a) => SomeClass (N a) where ...
Ahora, el sistema de clase de tipo de Haskell tiene un tipo específico adecuado para coincidir (es decir, N a
), en lugar de coincidir gratuitamente en cada tipo. Esto le permite controlar el alcance de la instancia ya que solo las cosas envueltas en el N
newtype ahora coincidirán.
Estoy diseñando una biblioteca que se beneficiaría enormemente del uso del indicador del compilador OverlappingInstances. Pero everyone hablan sobre esta extensión y advierten sobre sus peligros. Mi pregunta es, ¿hay ejemplos de un buen uso de esta extensión en algún lugar de hackage? ¿Hay alguna regla de oro sobre cómo encapsular la maldad y usar la extensión correctamente?
Quizás un experimento mental desmitifique un poco esta extensión.
Supongamos que hemos eliminado la restricción de que las funciones definidas con varios casos de patrones deben estar en un solo lugar, para que pueda escribir foo ("bar", Nothing) = ...
en la parte superior de un módulo, y luego tener casos como foo ("baz", Just x) = ...
otros lugares. De hecho, vamos más allá y permitimos que los casos se definan en diferentes módulos por completo .
Si crees que suena como si fuera confuso y propenso a errores de uso, estás en lo correcto.
Para recuperar algo de cordura, podríamos agregar algunas limitaciones. Por ejemplo (ha, ha), podríamos exigir que se cumplan las siguientes propiedades:
- En cualquier lugar donde se use una función, los argumentos proporcionados deben coincidir exactamente con un patrón. Cualquier otra cosa es un error del compilador.
- Agregar nuevos patrones (incluso importando otro módulo) nunca debe cambiar el significado de un código válido, ya sea que se elijan los mismos patrones o se produzca un error de compilación.
Debe quedar claro que hacer coincidir constructores simples como True
o Nothing
es sencillo. También podemos agitar las cosas un poco y suponer que el compilador puede desambiguar los literales, como "bar"
y "baz"
anteriores.
Por otro lado, los argumentos vinculantes con patrones como (x, Just y)
vuelven torpes: escribir dicho patrón significa renunciar a la capacidad de escribir patrones como (True, _)
o (False, Just "foobar")
más tarde, ya que Eso crearía ambigüedad. Peor aún, los guardias de patrón se vuelven casi inútiles, porque necesitan coincidencias muy generales. Muchos modismos comunes producirán infinitos dolores de cabeza por ambigüedad y, por supuesto, escribir un patrón de caída "por defecto" es completamente imposible.
Esto es más o menos la situación con las instancias de clase de tipo.
Podríamos recuperar algo de poder expresivo relajando las propiedades requeridas como tales:
- En cualquier lugar donde se utilice una función, debe coincidir con al menos un patrón. No hay coincidencias es un error del compilador.
- Si se usa una función tal que coincida con múltiples patrones, se utilizará el patrón más específico . Si no hay un patrón único más específico, se produce un error.
- Si una función se usa de manera que coincida con una instancia general, pero podría aplicarse en tiempo de ejecución a argumentos que coincidirían con una instancia más específica, esto es un error del compilador.
Tenga en cuenta que ahora nos encontramos en una situación en la que simplemente importar un módulo puede cambiar el comportamiento de una función, al poner en alcance un patrón nuevo y más específico. Las cosas también pueden volverse turbias en casos complicados que involucran funciones de orden superior. Sin embargo, en muchos casos los problemas son poco probables, por ejemplo, definir un patrón de caída genérico en una biblioteca, al tiempo que permite que el código del cliente agregue casos específicos si es necesario.
Eso es más o menos donde te pone OverlappingInstances
. Como se sugiere en el ejemplo anterior, si crear nuevas superposiciones siempre es imposible o deseable, y los diferentes módulos no terminarán viendo instancias diferentes y conflictivas, entonces es probable que esté bien.
Lo que realmente se debe a es que las limitaciones eliminadas por OverlappingInstances
están ahí para hacer que las clases de tipos se comporten de manera sensata bajo el supuesto de "mundo abierto" de que cualquier posible instancia podría agregarse más adelante. Al relajar esos requisitos, estás asumiendo esa carga tú mismo; así que piense en todas las formas en que se podrían agregar nuevas instancias y si alguno de esos escenarios es un problema importante. Si está convencido de que nada se interrumpirá en los casos de rincones oscuros y tortuosos, entonces siga adelante y use la extensión.
OverlappingInstances
te permite escribir muchas cosas útiles que de otra manera no serían implementables en el nivel de clase de tipos, aunque la gran mayoría de ellas pueden reorganizarse para usar una única dependencia funcional (escrita aquí en estilo polimórfico tipo)
class TypeEq (a :: k) (b :: k) (t :: Bool) | a b -> t where
typeEq :: Proxy a -> Proxy b -> HBool t
actualmente solo se puede implementar (de forma totalmente genérica) con OverlappingInstance
. Los ejemplos de casos de uso incluyen la codificación de Oleg de OOP en Haskell. Por lo tanto, mi único ejemplo de un buen uso de OverlappingInstances
es esta implementación de TypeEq
del documento HList clásico.
Esta funcionalidad particular podría proporcionarse de manera muy trivial con el soporte del compilador (e incluso trabajar en la función de tipo en lugar del nivel fundep) y, por lo tanto, no me parece tan malo pegar un solo módulo con TypeEq en alguna parte.
Cuando me involucro en una piratería de clases de tipo peligroso, a menudo encuentro que el comportamiento de IncoherentInstances
(elegir la primera instancia coincidente) es más fácil de razonar y más flexible, y por eso lo uso al menos en las fases de exploración de un diseño. Una vez que tengo algo que hace lo que quiero, trato de deshacerme de las extensiones, prestando especial atención a las que menos se comportan (como éstas).