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
.