繁体   English   中英

如何为类型类指定具体实现?

[英]How do I specify concrete implementations for a typeclass?

我有以下简单的Haskell模块,它定义了必须定义操作pushpoptop的类型类Queue ,以及空队列的构造函数和检查队列是否为空的函数。 然后它提供两种实现:先进先出队列和堆栈。

代码有效。 然而,似乎我在不必要地重复自己。 特别是,队列和堆栈之间唯一不同的操作是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

嗯,只有少量的重复,但让我们摆脱它。

关键是我们可以为Queue提供默认值,因为我们知道如何将其转换为列表,还提供了一个我们可以列出的队列。 因此,我们只需在您的定义中添加两个函数,即toListfromList ,并确保给予toListfromList ,或者给出其他函数,进行完整的定义。

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

如您所见,队列的任何实现都必须提供toListfromList或其他函数,因此两个队列的实现将变为:

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])

如果在Queue类型类中添加默认实现,则可以删除top的两个实现:

top = fst . pop

但除此之外,我认为这里没什么可做的。 无论如何,没有太多重复。

您关注的“重复”似乎是某些实现中的相似之处:

instance Queue Stack where
    empty = Stack []
    isEmpty (Stack xs) = null xs
    ...

instance Queue FifoQueue where
    empty = FifoQueue []
    isEmpty (FifoQueue xs) = null xs
    ...

但遗憾的是,没有办法合并这两个实例的部分内容。 你可以删除类型类,只需让StackFifoQueue成为同一类型的两个不同的构造函数。 从这里开始,HaskellElephant的解决方案主要适用(用lst代替toList )。

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 stacktop 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可以使这更安全。 外卖的信息是:每种方法都有自己的优点和缺点。 类型类型是一个很好的主意,但不要混淆它们的银色子弹; 了解其他选项,例如这些。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM