tipos preguntas investigacion icotonicas estadisticas estadistica encuestas ejemplos cuestionario con cerradas abiertas haskell types static-analysis typeclass

haskell - investigacion - tipos de preguntas cerradas



Clases de tipo cerradas (5)

¿Es posible crear una clase de tipos que ya no pueda admitir miembros nuevos (tal vez mediante el uso de límites de módulos)? Puedo negarme a exportar una función necesaria para una definición de instancia completa, pero eso solo da como resultado un error de tiempo de ejecución si alguien produce una instancia no válida. ¿Puedo convertirlo en un error de tiempo de compilación?


Creo que la respuesta es un sí calificado, dependiendo de lo que estés tratando de lograr.

Puede abstenerse de exportar el nombre de la clase de tipo desde su módulo de interfaz 1 , mientras sigue exportando los nombres de las funciones de clase de tipo. ¡Entonces nadie puede hacer una instancia de la clase porque nadie puede nombrarla!

Ejemplo:

module Foo ( foo, bar ) where class SecretClass a where foo :: a bar :: a -> a -> a instance SecretClass Int where foo = 3 bar = (+)

La desventaja es que nadie puede escribir un tipo con su clase como una restricción tampoco. Esto no impide por completo que las personas escriban funciones que tendrían ese tipo, porque el compilador aún podrá inferir el tipo. Pero sería muy molesto.

Puede mitigar la desventaja proporcionando otra clase de tipo vacía, con su clase "cerrada" como una súper clase. Haces que cada instancia de tu clase original también sea una instancia de la subclase, y exportas la subclase (junto con todas las funciones de clase de tipo), pero no la superclase. (Para mayor claridad, probablemente debería usar la clase "pública" en lugar de la "secreta" en todos los tipos que expone, pero creo que funciona de cualquier manera).

Ejemplo:

{-# LANGUAGE FlexibleInstances, UndecidableInstances #-} module Foo ( PublicClass, foo, bar ) where class SecretClass a where foo :: a bar :: a -> a -> a class SecretClass a => PublicClass a instance SecretClass Int where foo = 3 bar = (+) instance SecretClass a => PublicClass a

Puede prescindir de las extensiones si está dispuesto a declarar manualmente una instancia de PublicClass para cada instancia de SecretClass .

Ahora el código de cliente puede usar PublicClass para escribir restricciones de clase de tipo, pero cada instancia de PublicClass requiere una instancia de SecretClass para el mismo tipo, y sin forma de declarar una nueva instancia de SecretClass nadie puede hacer más instancias de tipos de PublicClass 2 .

Lo que no le proporciona todo esto es la capacidad del compilador para tratar la clase como "cerrada". Todavía se quejará de las variables de tipo ambiguas que podrían resolverse seleccionando la única instancia visible de "cerrado".

1 Opinión pura: por lo general, es una buena idea tener un módulo interno separado con un nombre aterrador que exporte todo para poder probarlo / depurarlo, con un módulo de interfaz que importe el módulo interno y solo exporte las cosas que desee exportar.

2 Supongo que con extensiones alguien podría declarar una nueva instancia superpuesta. Por ejemplo, si proporcionó una instancia para [a] , alguien podría declarar una nueva instancia de PublicClass para [Int] , lo que se aprovecharía de la instancia de SecretClass para [a] . Pero dado que PublicClass no tiene funciones y no pueden escribir una instancia de SecretClass , no veo que se pueda hacer mucho con eso.


Podría refactorizar la clase de tipo en una declaración de datos (use sintaxis de registro) que contenga todas las funciones que tenía su clase de tipo. Una lista fija de instancias parece que no necesitas una clase de todos modos.

Esto es, por supuesto, esencialmente lo que el compilador está haciendo para representar las escenas con tu clase de todos modos.

Esto le permitiría exportar la lista de instancias como funciones a su tipo de datos, y puede exportarlas, pero no los constructores para el tipo de datos. Del mismo modo, puede restringir la exportación de las funciones de acceso y solo exportar la interfaz que realmente desea.

Esto funciona bien porque los tipos de datos no están sujetos al supuesto de mundo abierto de cruce de límite de módulo que son las clases de tipos.

A veces, la complejidad del sistema de tipos hace que las cosas sean más difíciles.


Puede codificar clases de tipos cerradas a través de familias de tipos cerradas, que pueden codificarse esencialmente como familias de tipos asociados a su vez. La clave de esta solución es que las instancias de una familia de tipos asociada deben estar dentro de una instancia de clase de tipo, y solo puede haber una instancia de clase de tipo para cada tipo monomórfico.

Tenga en cuenta que este enfoque es independiente del sistema de módulos. En lugar de confiar en los límites del módulo, proporcionamos una lista explícita de qué instancias son legales. Esto significa, por un lado, que las instancias legales pueden distribuirse en múltiples módulos o incluso paquetes, y por otro lado, que no podemos proporcionar instancias ilegales incluso en el mismo módulo .

Para esta respuesta, supongo que queremos cerrar la siguiente clase para que solo pueda ser instanciada para el tipo Int e Integer , pero no para otros tipos:

-- not yet closed class Example a where method :: a -> a

Primero, necesitamos un pequeño marco para codificar familias de tipos cerradas como familias de tipos asociados.

{-# LANGUAGE TypeFamilies, EmptyDataDecls #-} class Closed c where type Instance c a

El parámetro c representa el nombre de la familia de tipos y el parámetro a es el índice de la familia de tipos. La instancia familiar de c para a está codificada como Instance ca Como c es un parámetro de clase, todas las instancias familiares de c deben darse juntas, en una sola declaración de instancia de clase.

Ahora, usamos este marco para definir una familia de tipo cerrado MemberOfExample para codificar que Int e Integer son Ok , y los demás tipos no.

data MemberOfExample data Ok instance Closed MemberOfExample where type Instance MemberOfExample Int = Ok type Instance MemberOfExample Integer = Ok

Finalmente, usamos esta familia de tipo cerrado en una superclase de nuestro Example .

class Instance MemberOfExample a ~ Ok => Example a where method :: a -> a

Podemos definir las instancias válidas para Int e Integer como de costumbre.

instance Example Int where method x = x + 1 instance Example Integer where method x = x + 1

Pero no podemos definir instancias no válidas para otros tipos que no sean Int e Integer .

-- GHC error: Couldn''t match type `Instance MemberOfExample Float'' with `Ok'' instance Example Float where method x = x + 1

Y tampoco podemos extender el conjunto de tipos válidos.

-- GHC error: Duplicate instance declarations instance Closed MemberOfExample where type Instance MemberOfExample Float = Ok -- GHC error: Associated type `Instance'' must be inside a class instance type instance Instance MemberOfExample Float = Ok

Desafortunadamente, podemos escribir la siguiente instancia falsa:

-- Unfortunately accepted instance Instance MemberOfExample Float ~ Ok => Example Float where method x = x + 1

Pero dado que nunca podremos cumplir con la restricción de igualdad, no creo que podamos usarla para nada. Por ejemplo, se rechaza lo siguiente:

-- Couldn''t match type `Instance MemberOfExample Float'' with `Ok'' test = method (pi :: Float)


Desde GHC 7.8.1, las familias de tipo cerrado pueden declararse, y creo que con la ayuda de ellos y ConstraintKinds , puede hacer esto:

type family SecretClass (a :: *) :: Constraint where SecretClass Int = ()

SecretClass a forma una restricción, equivalente a una clase de tipo, y dado que la familia no puede ser extendida por nadie, no se pueden definir otras instancias de la "clase".

(Esto es solo una especulación, ya que no puedo probarlo, pero el código en este enlace interesante hace que parezca que funcionaría).


Cuando todo lo que le interesa es que tiene un conjunto enumerado de instancias, este truco podría ayudar:

class (Elem t ''[Int, Integer, Bool] ~ True) => Closed t where type family Elem (t :: k) (ts :: [k]) :: Bool where Elem a ''[] = False Elem a (a '': as) = True Elem a (b '': bs) = Elem a bs instance Closed Int instance Closed Integer -- instance Closed Float -- ERROR