如何定义一个可以像堆栈一样推送和弹出的LIST?

时间:2014-08-02 02:43:32

标签: haskell

我的代码中定义了以下TYPE:

type Pos = (Int, Int)      -- position in 2 dimensions

我想创建一个包含Pos元素的列表(或堆栈或其他)。

我需要能够将一个Pos元素添加(推送)到列表的头部(当在该位置进行更改时),并且需要时从列表的头部拉出(弹出)一个Pos(到重播最后一次更改。)

我对Haskell真的很陌生,那么最好的方法是什么?我应该只使用一个列表并与HEAD一起玩吗?但是你如何去除"从头部(pop),或者是否有我可以使用的堆栈(对我来说听起来像堆栈类型的东西) - 如果是这样的话我如何用类型' Pos'?

1 个答案:

答案 0 :(得分:6)

听起来你想在列表上执行“有状态”操作,将其视为堆栈。 Haskell有几种方法可以做到这一点,最基本的是简单地定义带有堆栈的函数并为每个“修改”返回一个新函数,因为Haskell具有不可变变量(从技术上讲,它根本没有变量,只有名称绑定到不可变的值)。你可以这样做

newtype Stack a = Stack [a] deriving (Eq, Show)

这只是在[a]周围定义一个名为Stack a的包装器,因此我们的类型签名更加严格和信息丰富。然后,我们可以非常轻松地定义pushpop

pop :: Stack a -> (a, Stack a)
pop (Stack (x:xs)) = (x, Stack xs)
pop (Stack []) = error "Empty stack"

push :: a -> Stack a -> Stack a
push x (Stack xs) = Stack (x:xs)

这样可行,但我真的不喜欢pop能够引发error,这可能会导致程序崩溃。我们最好使用可以表示失败的数据类型,在这种情况下Maybe将是一个很好的选择:

pop :: Stack a -> (Maybe a, Stack a)
pop (Stack (x:xs)) = (Just x, Stack xs)
pop (Stack []) = (Nothing, Stack [])

我们可以像

一样使用它
main :: IO ()
main = do
    let start = Stack [] :: Stack Int
        step1 = push 1 start        -- Stack [1]
        step2 = push 2 step1        -- Stack [2, 1]
        (_, step3) = pop step2      -- Stack [1]
        step4 = push 10 step3       -- Stack [10, 1]
    print step4

但这真的很烦人,不能很好地构成,并要求我们编写大量的中间语句。当然,我们可以将这些push中的一些组合在一起,但它不会让我们受益匪浅。如果Haskell可以为我们处理那些中间值,那就更好了,事实上它可以。我们可以使用State monad来简化这一过程。 State monad代表一系列修改纯数据结构的计算。从本质上讲,它只是处理我们的所有组合和中间值,以便我们可以专注于算法,而不是细节。如果我们将pop重命名为pop_push重命名为push_,我们可以编写以下代码:

type StackState a b = State (Stack a) b

pop :: StackState a (Maybe a)
pop = state push_
-- The `state` function has the type (s -> (a, s)) -> State s a,
-- so applying it to `push_ :: Stack a -> (Maybe a, Stack a)` gives
-- us `State (Stack a) (Maybe a)`. (It actually has a bit more general
-- type, but it simplifies to this)

push :: a -> StackState a ()
push x = modify (push_ x)
-- The `modify` function has the type (s -> s) -> State s ()

现在我们可以更轻松地构建计算:

stackTransform :: StackState Int ()
stackTransform = do
    push 1
    push 2
    pop
    push 10

我们甚至可以编写更复杂的操作

test :: StackState Int Int
test = do
    mapM_ push [1..10]
    Just x <- pop
    push $ x * 10
    return x

然后这些可以从main作为

运行
main :: IO ()
main = do
    let start = Stack [] :: Stack Int
    print $ execState stackTransform start

虽然这个解决方案有点复杂并且需要一些monad知识,但它确实让我们更清晰地编写堆栈操作,而不必担心中间步骤,这是使用monad的一个好处。 State的实现细节有点棘手,所以我现在不会讨论它们,但是在某些时候它们非常好学习。


我认为还值得一提的是,还有另一种选择可以让你拥有“可变变量”,但你的所有行动都必须存在于IO monad中,而且它将是一种效率较低的实现比上面的任何一个。您可以使用充当可变指针的IORef来实现此行为:

import Data.IORef

newtype Stack a = Stack [a] deriving (Eq, Show)

type IOStack a = IORef (Stack a)

popIO :: IOStack a -> IO (Maybe a)
popIO iostack = do
    -- Read the current stack
    stack <- readIORef iostack
    case stack of
        Stack []     -> return Nothing
        Stack (x:xs) -> do
            -- Put the new stack back into the IORef
            writeIORef iostack (Stack xs)
            -- Return the top value in the stack
            return (Just x)

pushIO :: a -> IOStack a -> IO ()
pushIO x iostack = do
    -- Read the current stack
    (Stack xs) <- readIORef iostack
    -- Write the new stack back into the IORef
    writeIORef (Stack (x:xs))

然后您可以使用main作为

main :: IO ()
main = do
    iostart <- newIORef (Stack [] :: Stack Int)
    pushIO 1 iostart
    pushIO 2 iostart
    popIO iostart
    pushIO 10 iostart
    final <- readIORef iostart
    print final

但是这仍然比State版本的代码更多,并且它肯定更容易出错并且会更慢。