Haskell:引用更新函数中先前更新的列表元素

时间:2012-08-27 15:31:00

标签: haskell state

说我有以下定义

data Book = Book {id :: Int, title :: String}
type Shelf = [Book]

假设我有一个假设函数(upd用于更新)

updShelf :: Shelf -> Shelf
updShelf all@(book : books) = updBook book : updShelf books

到目前为止一切都很好。现在让我们说更新书功能需要参考 更新 之前预订三本书,即书架上第5位书的updateBook需要参考位置2的书(假设前三本书不需要这样的参考更新)。没问题,我说,并修改我的代码:

updShelf :: Shelf -> Shelf
updShelf all@(book : books) prevBook = updBook book prevBook : updShelf books
                where prevBook = ???

我需要帮助的是prevBook功能。虽然我甚至不确定我是否正确地接近这个问题。所以,如果你们有更好的建议以不同的方式解决这个问题,我们将非常感激

编辑:

Thomas M. DuBuisson:你的solution对我不起作用。原因如下: 假设初始货架(全部)状态为

Book {id=1, title="a"}
Book {id=2, title="b"}
Book {id=3, title="c"}
Book {id=4, title="d"}
Book {id=5, title="e"}
Book {id=6, title="f"}
Book {id=7, title="g"}
Book {id=8, title="h"}

然后(drop 3 partialUpdate)是(仅使用id而不是整本书语句):

updBook 4
updBook 5
updBook 6
updBook 7
updBook 8

zipWith'($)(drop 3 partialUpdate)(全部)是:

updBook 4 1
updBook 5 2
updBook 6 3
updBook 7 4 -> YIKES! Older version of book 4!
updBook 8 5 -> YIKES! Older version of book 5!

就我而言,我需要根据第4和第5册的已更新版本更新第7和第8本书,而不是未更新的第7和第5本。我希望你理解我的意思。

6 个答案:

答案 0 :(得分:11)

这个技巧与tying the knot有关:我们将在计算答案时使用答案。出于说明的目的,我将改为使用type Book = Int

updateShelf :: Shelf -> Shelf
updateShelf shelf = answer where
   answer  = zipWith updateBook shifted shelf
   shifted = replicate 3 Nothing ++ map Just answer

-- some stupid implementation just for illustration
updateBook :: Maybe Book -> Book -> Book
updateBook Nothing          current = current + 1
updateBook (Just threeBack) current = current + threeBack + 1

现在,在ghci中,我们可以验证updateShelf是否确实使用了更新版本:

*Main> updateShelf [1,10,100,1000,10000]
[2,11,101,1003,10012]

如您所见,前三个是1+110+1100+1,其余两个是1000+(1+1)+110000+(10+1)+1,因此,正如您所希望的那样,使用更新的先前值。

答案 1 :(得分:6)

这里没有结点,没有信息回流,没有传递对尚未定义的值的前向引用。恰恰相反,我们回顾已知的已经计算的值。它甚至没有懒惰的评估。这样:

updShelf shelf@(a : b : c : t) = xs where
   xs = a : b : c : zipWith updBook t xs

就是你所需要的。所有它正在“做”,是将显式后向指针保持在正在生成的序列中,返回三个槽口。 “后退指针非常简单”,引用haskellwiki's page来打结。

updBook的每次调用都会在此处收到两个参数 - 一个是原始列表中位置i+3的一个项目,另一个是位置i的新更新项目。

将它与这段Haskell传说进行比较:

g (a,b) = xs where
   xs = a : b : zipWith (+) xs (tail xs)

这里也没有打结。

答案 2 :(得分:5)

首先:您的updShelfmap updBook

第二:我认为书籍列表可能不是您问题的最佳数据结构,因为列表不支持随机访问操作。如果您在计算中的任何时候确实需要随机访问,您可能想尝试使用数组 - 请参阅Data.Array

现在我的主要观点:你正在尝试做的事情 - 有一个计算消耗部分自己的结果 - 在Haskell社区经常被称为“打结”或“信用卡转型“(立即购买,稍后付款)。基本上,它归结为以下类型的表达式在Haskell中有效:

let result = f result in result  -- apply f to its own result

这怎么可能有效?好吧,首先,正如你当然所知,Haskell是懒惰的,所以这样的计算不是必然循环。其次,它不适用于任何计算;必须有一个非循环的终止顺序,其中可以执行计算的子步骤。

例如,此cycle函数通过将xs的结果附加到cycle xs来生成xs的循环列表:

cycle xs = let result = xs ++ result in result

“绑结”模式可以使用库函数fix进行抽象,我在这里展示了它的用法:

fix f = let result = f result in result
cycle xs = fix (xs++)

所以在你的情况下,我建议的是:

  • 使用惰性数组(如Haskell的内置Array类型)来表示Shelf;这使您可以随机访问元素。
  • 使用结打结技术在计算过程中引用结果Array

答案 3 :(得分:2)

你可以分两部分来做,首先你将updBook部分应用到所有书籍,然后制作一个功能列表,然后你通过一个zip将这些功能应用到正确的前一本书:

updShelf :: Shelf -> Shelf
updShelf [] = []
updShelf all@(book:books) = 
    let partialUpdate = map updBook all :: Book -> Shelf
    in zipWith ($) (drop 3 partialUpdate) all

updBook :: Book -> Book -> Shelf
updBook = undefined

请注意,这在开始时有奇怪的行为 - 由于drop 3,它只会使新列表中的三个元素更短。这不是必需的,您可以将该行读为zipWith ($) partialUPdate (drop (len-3) (cycle all)),但您从未指定列表开头没有旧书的情况。

答案 4 :(得分:2)

作为一个递归函数,updShelf将无法访问尚未传递的元素(因此,如果它获得一个从bookFive开始的列表,它将无法访问到bookThree

如果要以递归方式编写函数,则需要进行更多模式匹配:

updShelf :: Shelf -> Shelf
updShelf (bookOne : bookTwo: bookThree : books) 
    = (updBook bookOne bookThree) : (updShelf $ bookTwo : bookThree : books)

updBook :: Book -> Book -> Shelf
updBook twoEarlier current = {- undefined -}

答案 5 :(得分:2)

好的,这是实现这一目标的灵活方式:

updateOne book = undefined
updateTwo prevUpdatedBook book = undefined

updateBooks books = answer
  where numEasy = 3
        (easy,hard) = splitAt numEasy books
        answer = mapOnto updateOne easy (zipWith updateTwo answer hard)

-- More efficient that (map f xs ++ end)
mapOnto f xs end = foldr ((:) . f) end xs