haskell typeclass

Cómo "extender" las clases en Haskell



typeclass (3)

Con GHC 8.6 y superior, esto también se puede lograr a través de DerivingVia :

{-# LANGUAGE DerivingVia #-} {-# LANGUAGE GeneralisedNewtypeDeriving #-} {-# LANGUAGE StandaloneDeriving #-} -- Class definitions: class IsEven a where isEven :: a -> Bool -- Note that we don''t need to have IsEven as a superclass. class DivisibleBy a where divisibleBy :: a -> Int -> Bool -- Boilerplate that only needs to be written once: -- Boilerplate DivisibleBy instance generated with GeneralisedNewtypeDeriving. newtype WrappedDivisibleBy a = WrapDivisibleBy { unwrapDivisibleBy :: a } deriving DivisibleBy instance DivisibleBy a => IsEven (WrappedDivisibleBy a) where isEven n = n `divisibleBy` 2 -- Instance example: instance DivisibleBy Int where divisibleBy a i = a `mod` i == 0 -- Boilerplate IsEven instance generated with DerivingVia -- (and StandaloneDeriving, as we aren''t defining Int here). deriving via (WrappedDivisibleBy Int) instance IsEven Int

DerivingVia no siempre es una opción (en el caso de clases como Traversable , que tienen un constructor de tipo extra que envuelve cosas en la firma de tipo, choca con el sistema de roles); Cuando funciona, sin embargo, es muy limpio.

Quiero crear dos clases de tipos, A y B , donde A es una superclase de B Las funciones definidas en B son suficientes para implementar las de A Luego, si tengo una función con la restricción fun :: (A thing) => ... una instancia de B para, digamos, Int , me gustaría poder pasar un Int a fun sin crear una instancia duplicada A para Int .

Por ejemplo, digamos que tengo una clase de tipo que puede verificar si el valor es "par". Luego, tengo otra clase de tipo que puede verificar si un valor es divisible por algún número. La segunda clase de tipos es lo suficientemente potente como para implementar las funciones en la primera, y cualquier función que solo requiera capacidades de "comprobación par" debe poder aceptar un argumento que tenga capacidades de "divisible por".

Esto es lo que creo que sería:

class IsEven a where isEven :: a -> Bool class (IsEven a) => DivisibleBy a where divisibleBy :: a -> Int -> Bool isEven :: a -> Bool isEven a = divisibleBy a 2 printIsEven :: (IsEven a) => a -> IO () printIsEven a = putStrLn (show (IsEven.isEven a)) instance IsEven Int -- I need to do this or I cannot create a DivisibleBy instance instance DivisibleBy Int where divisibleBy a i = a `mod` i == 0 myint :: Int myint = 2 main :: IO () main = printIsEven myint

Sin embargo, en tiempo de compilación esto produce la advertencia:

[2 of 2] Compiling Main ( Foo.hs, Foo.o ) Foo.hs:11:10: warning: [-Wmissing-methods] • No explicit implementation for ‘IsEven.isEven’ • In the instance declaration for ‘IsEven Int’ | 11 | instance IsEven Int | ^^^^^^^^^^ Linking Foo ...

y en el tiempo de ejecución, el programa falla:

Foo: Foo.hs:11:10-19: No instance nor default method for class operation isEven

¿Cómo puedo lograr este efecto de subtipo sin duplicar la lógica en una instance IsEven ?


No puede redefinir un método en una clase nueva y hacer que afecte al método en la clase anterior. Si desea que los métodos funcionen de esta manera, la clase principal debe hacer referencia a la clase secundaria.

Necesita la extensión DefaultSignatures para que esto funcione. Enciéndelo y luego cambia tus clases a esto:

class IsEven a where isEven :: a -> Bool default isEven :: DivisibleBy a => a -> Bool isEven a = divisibleBy a 2 class IsEven a => DivisibleBy a where divisibleBy :: a -> Int -> Bool


Por lo que sé, lo más cerca que puedes estar en Haskell estándar es

instance IsEven Int where isEven n = n `divisibleBy` 2 instance DivisibleBy Int where divisibleBy a i = a `mod` i == 0

No tiene que duplicar la lógica (de hecho, puede implementar isEven en términos de divisibleBy ), pero aún debe proporcionar una definición explícita.

Tendría que repetir este patrón para cada tipo que desee crear una instancia de DivisibleBy .

Usando la extensión de idioma DefaultSignatures también puede hacer lo siguiente:

{-# LANGUAGE DefaultSignatures #-} class IsEven a where isEven :: a -> Bool default isEven :: (DivisibleBy a) => a -> Bool isEven n = n `divisibleBy` 2 class (IsEven a) => DivisibleBy a where divisibleBy :: a -> Int -> Bool instance IsEven Int instance DivisibleBy Int where divisibleBy a i = a `mod` i == 0

Esto mueve la implementación por defecto a la clase en sí. Ahora puede decir la instance IsEven Int sin proporcionar un cuerpo de instancia. La desventaja es que ahora IsEven tiene que saber acerca de DivisibleBy y solo puede proporcionar una implementación default .