Кажется, что "дублирование", о котором вы беспокоитесь, является сходством в некоторых реализациях:
instance Queue Stack where
empty = Stack []
isEmpty (Stack xs) = null xs
...
instance Queue FifoQueue where
empty = FifoQueue []
isEmpty (FifoQueue xs) = null xs
...
Но, к сожалению, просто нет способа объединить части этих двух экземпляров. Вы можете удалить класс типов и просто иметь Stack
и FifoQueue
двумя разными конструкторами одного типа. Отсюда в основном применяются решения HaskellElephant (замена toList
на lst
).
data Queue a = Stack { lst :: [a] }
| FifoQueue { lst :: [a] }
deriving (Eq, Show)
-- "empty" obviously cannot be preserved as it was
-- you need to specify whether you want an empty Stack or empty FifoQueue
emptyS = Stack []
emptyQ = FifoQueue []
-- but some functions are the same either way
isEmpty = null . lst
top queue = head . lst
-- other functions behave *mostly* the same for both cases...
pop queue = (top queue, liftQ tail queue)
-- ...they just need a little helper to abstract over the slight difference
liftQ :: ([a] -> [b]) -> Queue a -> Queue b
liftQ f (Stack xs) = Stack (f xs)
liftQ f (FifoQueue xs) = FifoQueue (f xs)
-- then for functions where the implementation is completely different,
-- you just pattern match
push x (Stack xs) = Stack (x:xs)
push x (FifoQueue xs) = FifoQueue (xs ++ [x]) -- this is slow, by the way
Недостатком этого является, конечно, то, что вместо открытого класса типов ваш модуль теперь предоставляет закрытый ADT.
Там есть какая-то золотая середина, хотя. Вроде, как бы, что-то вроде. Рассмотрим этот альтернативный подход:
data QueueImpl q a = QueueImpl { _empty :: q a
, _isEmpty :: q a -> Bool
, _top :: q a -> a
, _pop :: q a -> (a, q a)
, _push :: a -> q a -> q a
}
-- partially applied constructor!
shared :: (a -> [a] -> [a]) -> QueueImpl [] a
shared = QueueImpl empty' isEmpty' top' pop'
where empty' = []
isEmpty' = null
top' = head
pop' (x:xs) = (x, xs)
stack :: QueueImpl [] a
stack = shared push'
where push' = (:)
fifoQueue :: QueueImpl [] a
fifoQueue = shared push'
where push' x = (++[x])
Превратив класс типов в тип данных, мы можем частично применить конструктор, таким образом разделяя реализации для большинства методов. Подвох в том, что у нас нет доступа к функциям, которые полиморфны так же, как и раньше. Для доступа к методам нам нужно сделать top stack
или top fifoQueue
. Это приводит к некоторым интересным изменениям в разработке «полиморфных» функций: поскольку мы усовершенствовали класс типов, нам нужно явно передать реализацию любым составным функциям:
-- if you haven't figured out by now, "impl" is short for "implementation"
_push3 :: QueueImpl [] a -> a -> [a] -> [a]
_push3 impl x = push x . push x . push x
where push = _push impl
-- _push3 as implemented by a stack:
sPush3 :: a -> [a] -> [a]
sPush3 = _push3 stack
Обратите внимание, что здесь мы проигрываем в некоторой безопасности типов; представление как стека, так и FifoQueue отображается в виде необработанного списка. Там могут быть некоторые хакеры нового типа, которые могут сделать это немного безопаснее. Вывод таков: у каждого подхода есть свои преимущества и недостатки. Классы типов - довольно хорошая идея, но не путайте их с Серебряной пулей; знать о других опциях, таких как эти.