我有以下简单的Haskell模块,它定义了类型Queue
,必须在其上定义操作push
,pop
和top
,以及构造函数空队列和检查队列是否为空的函数。然后它提供两种实现:先进先出队列和堆栈。
代码有效。然而,似乎我在不必要地重复自己。特别是,队列和堆栈之间唯一不同的操作是push
操作(我们将新对象推送到列表的前面还是后面?)。似乎应该有一些方法来定义类型类定义中的常见操作。事实上,这可能吗?
module Queue (
Queue,
FifoQueue(FifoQueue),
Stack(Stack),
empty,
isEmpty,
push,
pop,
top
) where
class Queue q where
empty :: q a
isEmpty :: q a -> Bool
push :: a -> q a -> q a
pop :: q a -> (a, q a)
top :: q a -> a
data Stack a = Stack [a] deriving (Show, Eq)
instance Queue Stack where
empty = Stack []
isEmpty (Stack xs) = null xs
push x (Stack xs) = Stack (x:xs)
pop (Stack xs) = (head xs, Stack (tail xs))
top (Stack xs) = head xs
data FifoQueue a = FifoQueue [a] deriving (Show, Eq)
instance Queue FifoQueue where
empty = FifoQueue []
isEmpty (FifoQueue xs) = null xs
push x (FifoQueue xs) = FifoQueue (xs ++ [x])
pop (FifoQueue xs) = (head xs, FifoQueue (tail xs))
top (FifoQueue xs) = head xs
答案 0 :(得分:5)
嗯,只有少量重复,但让我们摆脱它。
关键是我们可以提供Queue
的默认值,因为我们知道如何将其转换为列表,并且还提供了我们可以列出的队列。因此,我们只需在您的定义中添加两个函数toList
和fromList
,并确保给予toList
和fromList
,或者给出其他函数,做出完整的定义
import Control.Arrow
class Queue q where
empty :: q a
empty = fromList []
isEmpty :: q a -> Bool
isEmpty = null . toList
push :: a -> q a -> q a
push a b = fromList (a : toList b)
pop :: q a -> (a, q a)
pop qs = (head . toList $ qs,fromList . tail . toList $ qs)
top :: q a -> a
top = head . toList
toList :: q a -> [a]
toList queue = if isEmpty queue then []
else uncurry (:) . second toList . pop $ queue
fromList :: [a] -> q a
fromList = foldr push empty
如您所见,任何队列实现都必须提供toList
和fromList
或
其他函数,因此两个队列的实现如下:
data Stack a = Stack [a] deriving (Show, Eq)
instance Queue Stack where
toList (Stack a) = a
fromList a = Stack a
data FifoQueue a = FifoQueue [a] deriving (Show, Eq)
instance Queue FifoQueue where
toList (FifoQueue a) = a
fromList a = FifoQueue a
push x (FifoQueue xs) = FifoQueue (xs ++ [x])
答案 1 :(得分:3)
如果在top
类型类中添加默认实现,则可以删除Queue
的两个实现:
top = fst . pop
但除此之外,我认为这里没什么可做的。无论如何,没有太多重复。
答案 2 :(得分:3)
您关注的“重复”似乎是某些实施中的相似性:
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
请注意,我们在这里失去了一些类型的安全性; Stack和FifoQueue的表示形式作为原始列表公开。可能会有一些新类型的hackery可以使这更安全。外卖的信息是:每种方法都有自己的优点和缺点。类型类型是一个很好的主意,但不要混淆它们的银色子弹;请注意其他选项,例如这些。