记忆一个将set作为参数的函数

时间:2015-08-09 20:51:32

标签: haskell memoization

我正在使用Data.MemoCombinatorshttps://hackage.haskell.org/package/data-memocombinators-0.3/docs/Data-MemoCombinators.html)来记忆一个函数,该函数将一个集合作为其参数并返回一个集合(这是一个人为的例子,什么都不做,但需要很长时间才能完成) :

test s = case Set.toList s of
         []     -> Set.singleton 0
         [x]    -> Set.singleton 1
         (x:xs) -> test (Set.singleton x) `Set.union` test (Set.fromList xs)

由于Data.MemoCombinators没有实现集合的表,我想写自己的:

{-# LANGUAGE RankNTypes #-}

import Data.MemoCombinators (Memo)
import qualified Data.MemoCombinators as Memo
import Data.Set (Set)
import qualified Data.Set as Set

set :: Ord a => Memo a -> ((Set a) -> r) -> (Set a) -> r
set m f = Memo.list m (f . Set.fromList) . Set.toList

这是我应该记住的test

test s = set Memo.integral test' s
    where
      test' s = case Set.toList s of
                []     -> Set.singleton 0
                [x]    -> Set.singleton 1
                (x:xs) -> test (Set.singleton x) `Set.union` test (Set.fromList xs)

我没有明确Data.MemoCombinators的文档,所以我基本上不知道自己在做什么。

我的问题是:

  1. Memo.list函数的第二个参数是什么?它是列表元素的备忘录吗?

  2. 如何直接为集合实现表,而不使用Memo.list?这里想弄清楚如何在不使用某人的库的情况下手动实现memoization。例如,使用Map。我见过使用无限列表记忆整数的例子但是在地图的情况下我无法弄清楚如何初始化地图以及如何插入地图。

  3. 感谢您的帮助。

2 个答案:

答案 0 :(得分:2)

test的内容很好,但通常您会使用Set操作将test定义为集合上的函数。以下是我所说的一个例子:

-- memoize a function on Set Int
foo = set M.integral foo'
  where foo' s | Set.null s = 0
        foo' s = let a = Set.findMin s
                     b = Set.findMax s
                     m = (a+b) `div` 2
                     (lo,found,hi) = Set.splitMember m s
                 in if a >= b
                      then 1
                      else (if found then 1 else 0) + foo lo + foo hi

这是计算集合中元素数量的一种非常低效的方法,但请注意如何根据集合操作定义foo'

回答你的其他问题:

  
      
  1. Memo.list函数的第二个参数是什么?它是列表元素的记忆器吗?
  2.   

Memo.list有签名Memo a -> Memo [a],因此在Memo.list m f表达式中我们有:

m :: Memo a
f :: [a] -> r    -- some type r
Memo.list m f :: [a] -> r

因此,f是您要记住的[a]上的函数,而m是带有a类型参数的函数的记事本。

  
      
  1. 如何直接为集合实现表?
  2.   

这取决于你所说的“直接”。以这种方式进行记忆将涉及创建(可能是无限的)惰性数据结构。 stringintegrallist备忘录都使用某种形式的懒惰特里。这与命令式语言中的memoization非常不同,在命令式语言中,您显式检查哈希映射以查看是否已经计算了某些内容并使用函数的值等更新该哈希映射。(顺便说一下 - 你可以在ST中进行那种记忆化或IO monad,它可能比Data.Memocombinators方法更好 - 需要考虑的事情。)

通过传递给列表来记住Set a -> r函数的想法是一个好主意,但我会使用/来自AscList:

set m f = Memo.list m (f . Set.fromAscList) . Set.toAscList

这样,集合Set.fromList [3,4,5]将重新使用为记忆Set.fromList [3,4]的值而创建的trie的相同部分。

答案 1 :(得分:2)

  
      
  1. Memo.list函数的第二个参数是什么?它是列表元素的记忆器吗?
  2.   

第一个参数m是列表元素的memoizer。第二个参数f是您要应用于列表的函数(也将被记忆)。

  
      
  1. 如何直接为集合实现表,而不使用Memo.list?这里想弄清楚如何实现   手动记忆而不使用某人的图书馆。例如,   使用地图。我见过使用a来记忆整数的例子   无限列表,但在地图的情况下,我无法弄清楚如何   初始化地图以及如何插入地图。
  2.   

使用Data.MemoCombinators的相同策略,您可以执行类似于希望它们用于列表的操作。这种方法没有使用明确的数据结构,但探讨了Haskell将内容保存在内存和惰性评估中的方式。

set :: Ord a => Memo a -> Memo (Set a)
set m f = table (f Set.empty) (m (\x -> set m (f . (x `Set.insert`))))
  where
  table nil cons set | Set.null set = nil
                     | otherwise    = uncurry cons (Set.deleteFindMin set)

您还可以使用显式数据结构(如Map)在Haskell中使用memoization。我将使用Fibonacci示例来证明,因为它更容易进行基准测试,但它与其他函数类似。

让我们从天真的实现开始:

fib0 :: Integer -> Integer
fib0 0 = 0
fib0 1 = 1
fib0 x = fib0 (x-1) + fib0 (x-2)

然后Data.MemoCombinators提出了这个实现:

import qualified Data.MemoCombinators as Memo

fib1 :: Integer -> Integer
fib1 = Memo.integral fib'
  where
  fib' 0 = 0
  fib' 1 = 1
  fib' x = fib1 (x-1) + fib1 (x-2)

最后,我的版本使用Map

import Data.Map (Map)
import qualified Data.Map as Map

fib2 :: Integer -> Integer
fib2 = fst . fib' (Map.fromList [(0, 0),(1, 1)])
  where
  fib' m0 x | x `Map.member` m0 = (Map.findWithDefault 0 x m0, m0)
            | otherwise         = let (v1, m1) = fib' m0 (x-1)
                                      (v2, m2) = fib' m1 (x-2)
                                      y = v1 + v2
                                  in (y, Map.insert x y m2)

现在,让我们看看他们的表现如何:

fib0 40: 13.529371s
fib1 40: 0.000121s
fib2 40: 0.000048s

fib0已经太慢了。让我们对其他两个进行适当的测试:

fib1 400000: 6.234243s
fib2 400000: 4.022798s

fib1 500000: 8.683649s
fib2 500000: 5.781104s

对于我执行的所有测试,Map解决方案实际上似乎优于Memo解决方案。但我认为Data.MemoCombinators的最大优势实际上是具有这种出色的性能而无需编写比天真解决方案更多的代码。

更新:我改变了结论,因为我没有正确地做基准测试。我在同一个执行中做了几次调用,在500000的情况下,无论是第二次调用(fib1还是fib2),都花了太长时间。