Как «расширить» классы в Haskell - PullRequest
9 голосов
/ 06 июля 2019

Я хочу создать два класса типов, A и B, где A - суперкласс B.Функции, определенные в B, достаточны для реализации функций в A.Затем, если у меня есть функция с ограничением fun :: (A thing) => ... экземпляр B для, скажем, Int, я хотел бы иметь возможность передать Int в fun без создания дублирующего экземпляра A для Int.

Например, допустим, у меня есть класс типов, который может проверить, является ли значение "четным".Затем у меня есть другой класс типов, который может проверить, делится ли значение на некоторое число.Класс второго типа является достаточно мощным для реализации функций первого, и любая функция, которая требует только возможности «четной проверки», должна иметь возможность принимать аргумент, обладающий способностями «делимого на».

Воткак мне кажется, это будет выглядеть так:

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

Однако во время компиляции выдается предупреждение:

[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 ...

и во время выполнения программа завершается ошибкой:

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

Как мне добиться этого эффекта подтипа, не дублируя логику в instance IsEven?

Ответы [ 3 ]

12 голосов
/ 06 июля 2019

Насколько я знаю, самое близкое, что вы можете получить в стандартном Haskell, это

instance IsEven Int where
  isEven n = n `divisibleBy` 2

instance DivisibleBy Int where
  divisibleBy a i = a `mod` i == 0

Вам не нужно дублировать логику (действительно, вы можете реализовать isEven в терминах divisibleBy), но вам все равно нужно предоставить явное определение.

Вам придется повторять этот шаблон для каждого типа, для которого вы хотите создать экземпляр DivisibleBy.

Использование DefaultSignatures расширение языка Вы также можете сделать следующее:

{-# 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

Это перемещает реализацию по умолчанию в сам класс.Теперь вы действительно можете просто сказать instance IsEven Int без указания экземпляра тела.Недостатком является то, что теперь IsEven должен знать о DivisibleBy, и вы можете предоставить только одну реализацию default.

6 голосов
/ 06 июля 2019

Вы не можете переопределить метод в новом классе и заставить его влиять на метод в старом классе.Если вы хотите, чтобы методы работали так, родительский класс должен ссылаться на дочерний класс.

Вам нужно расширение DefaultSignatures , чтобы эта работа работала.Включите его, а затем измените ваши классы на это:

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
4 голосов
/ 06 июля 2019

С GHC 8.6 и выше, это также может быть достигнуто через 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 не всегда является опцией (в случае таких классов, как Traversable, у которых есть дополнительный конструктор типов, заключающий вещи в сигнатуру типа, он конфликтует с системой ролей); когда это работает, тем не менее, это очень опрятно.

...