在Haskell中实现memoization函数

时间:2017-06-10 18:45:22

标签: haskell memoization

我对Haskell相当新,我试图实现一个基本的memoization函数,该函数使用Data.Map来存储计算值。我的例子是Project Euler Problem 15,它涉及在20x20网格中计算从1个角到另一个角的可能路径的数量。

这是我到目前为止所拥有的。我还没有尝试编译,因为我知道它不会编译。我将在下面解释。

import qualified Data.Map as Map

main = print getProblem15Value

getProblem15Value :: Integer
getProblem15Value = getNumberOfPaths 20 20

getNumberOfPaths :: Integer -> Integer -> Integer
getNumberOfPaths x y = memoize getNumberOfPaths' (x,y)
where getNumberOfPaths' mem (0,_) = 1
      getNumberOfPaths' mem (_,0) = 1
      getNumberOfPaths' mem (x,y) = (mem (x-1,y)) + (mem (x,y-1))

memoize :: ((a -> b) -> a -> b) -> a -> b
memoize func x = fst $ memoize' Map.Empty func x
    where memoize' map func' x' = case (Map.lookup x' map) of (Just y) -> (y, map)
                                                              Nothing -> (y', map'')
           where y' = func' mem x'
                 mem x'' = y''
                 (y'', map') = memoize' map func' x''
                 map'' = Map.insert x' y' map'

所以基本上,我的结构方式是memoize是一个组合者(根据我的理解)。 memoization有效,因为memoize提供了一个函数(在本例中为getNumberOfPaths'),函数调用(mem)进行递归,而不是getNumberOfPaths'调用自身,这将在第一次迭代后删除memoization。

我对memoize的实现需要一个函数(在本例中为getNumberOfPaths')和一个初始值(在这种情况下是一个元组(x,y),表示距离另一个角的网格单元格距离的数量的网格)。它调用具有相同结构的memoize',但包含一个空Map来保存值,并返回一个包含返回值和新计算Map的元组。 memoize'执行地图查找,如果存在值,则返回值和原始地图。如果没有值,则返回计算值和新映射。

这是我的算法失败的地方。要计算新值,我使用func'getNumberOfPaths'调用memx')。 mem只返回y'',其中y''包含在再次调用memoize'的结果中。 memoize'还会返回一个新地图,然后我们会向其添加新值并将其用作memoize'的返回值。

此处的问题是,(y'', map') = memoize' map func' x''行应位于mem下,因为它取决于x''memmap'的参数。我当然可以这样做,但之后我将失去Map值,这是我需要的,因为它包含来自中间计算的记忆值。但是,我不想将mem引入memoize的返回值,因为传递给Map的函数必须处理memoize。< / p>

很抱歉,如果这听起来很混乱。很多这种超高阶功能的东西让我感到困惑。

我确信有办法做到这一点。我想要的是一个通用的getNumberOfPaths函数,它允许递归调用,就像在{{1}}的定义中一样,计算逻辑不必完全关心memoization是如何完成的。

5 个答案:

答案 0 :(得分:3)

如果您的输入足够小,您可以做的一件事是将备忘表分配为Array而不是Map,提前包含所有结果,但是懒惰地计算:

import Data.Array ((!), array)

numPaths :: Integer -> Integer -> Integer
numPaths w h = get (w - 1) (h - 1)
  where

    table = array (0, w * h)
      [ (y * w + x, go x y)
      | y <- [0 .. h - 1]
      , x <- [0 .. w - 1]
      ]

    get x y = table ! fromInteger (y * w + x)

    go 0 _ = 1
    go _ 0 = 1
    go x y = get (x - 1) y + get x (y - 1)

如果您愿意,也可以将其拆分为单独的函数:

numPaths w h = withTable w h go (w - 1) (h - 1)
  where
    go mem 0 _ = 1
    go mem _ 0 = 1
    go mem x y = mem (x - 1) y + mem x (y - 1)

withTable w h f = f'
  where
    f' = f get
    get x y = table ! fromInteger (y * w + x)
    table = makeTable w h f'

makeTable w h f = array (0, w * h)
  [ (y * w + x, f x y)
  | y <- [0 .. w - 1]
  , x <- [0 .. h - 1]
  ]

我不会为你破坏它,但也有一个非递归的答案公式。

答案 1 :(得分:1)

您无法实施memoize :: ((a -> b) -> a -> b) -> a -> b。为了存储某些a的结果,您需要在a的内存中占有一席之地,这意味着您需要知道那些是什么a是。{/ p>

一种火腿式的方法是为你知道所有值的类型添加一个类型类,如Universe

class Universe a where
    universe :: [a]

然后,您可以通过为memoize :: (Ord a, Universe a) => ((a -> b) -> a -> b) -> a -> b中的每个Map值构建一个包含b值的a来实现universe :: [a],通过传递来创建备忘函数地图查找到func,并通过声明它们使用备忘录函数来填充b

这对Integer不起作用,因为它们的数量有限。它甚至不能为Int工作,因为它们太多了。要记住Integer等类型,您可以使用MemoTrie中使用的方法。构建一个惰性无限数据结构,用于保存叶子上的值。

这是Integer s的一种可能结构。

data IntegerTrie b = IntegerTrie {
    negative :: [b],
    zero :: b,
    positive :: [b]
}

更高效的结构允许深入跳入线索以避免指数时间查找。对于Integers,MemoTrie采用将密钥转换为具有一对函数a -> [Bool][Bool] -> a并使用大约以下trie的位列表的方法。

data BitsTrie b = BitsTrie {
    nil :: b,
    false :: BitsTrie b,
    true :: BitsTrie b
}

MemoTrie继续抽象出具有一些可用于记忆它们的关联trie的类型,并提供了将它们组合在一起的方法。

答案 2 :(得分:1)

这可能无法直接帮助您实施备忘录,但您可以使用其他人的... monad-memo。调整他们的一个例子......

{-# LANGUAGE FlexibleContexts #-}

import Control.Monad.Memo

main = print $ startEvalMemo (getNumberOfPaths 20 20)

getNumberOfPaths :: (MonadMemo (Integer, Integer) Integer m) => Integer -> Integer -> m Integer
getNumberOfPaths 0 _ = return 1
getNumberOfPaths _ 0 = return 1
getNumberOfPaths x y = do
  n1 <- for2 memo getNumberOfPaths (x-1) y
  n2 <- for2 memo getNumberOfPaths x (y-1)
  return (n1 + n2)

......我怀疑你可以在源https://github.com/EduardSergeev/monad-memo

中实现类似的东西

答案 3 :(得分:0)

  

但是,我不想将Map引入mem的返回值,因为传递给memoize的函数必须处理Map。

如果我理解,你将不得不做这样的某事,至少如果你的目的是将记忆值存储在一个地图中,该地图会被复制到每个找到的新值上。提醒注意我认为没有意义的东西......

getNumberOfPaths' mem (x,y) = (mem (x-1,y)) + (mem (x,y-1))

...表示来自一个分支mem (x-1,y)的任何记忆都不能在其他mem (x,y-1)中使用,因为两者中都将使用相同的mem,包含相同的mem信息,whatevever价值/功能Integer最终成为。不知何故,您必须将memoized值从一个传递到另一个。这意味着调用recurse的函数不能只返回Integer:它必须返回Integer以及与getNumberOfPaths :: (Integer, Integer) -> Integer getNumberOfPaths (x, y) = snd $ memoize Map.empty getNumberOfPaths' (x, y) getNumberOfPaths' :: Map.Map (Integer, Integer) Integer -> (Integer, Integer) -> (Map.Map (Integer, Integer) Integer, Integer) getNumberOfPaths' map (0,_) = (map, 1) getNumberOfPaths' map (_,0) = (map, 1) getNumberOfPaths' map (x,y) = (map'', first + second) where (map', first) = memoize map getNumberOfPaths' (x-1, y) (map'', second) = memoize map' getNumberOfPaths' (x, y-1) memoize :: Ord a => Map.Map a b -> (Map.Map a b -> a -> (Map.Map a b, b)) -> a -> (Map.Map a b, b) memoize map f x = case Map.lookup x map of (Just y) -> (map, y) Nothing -> (map'', y) where (map', y) = f map x map'' = Map.insert x y map' 一起发现的记忆值的一些知识。

有很多方法可以做到这一点。虽然由于memoization细节的传播可能不受欢迎,但您可以明确地传递地图。

getNumberOfPaths'

memoize确实需要传递地图,并且需要知道它的签名,但至少它不需要与地图交互:这是在Maybe中完成的,所以我不要以为坏了。

我想如果你只想传递一个函数,你可以。您可以将一系列函数用作穷人的地图,但他们必须返回getNumberOfPaths :: (Integer, Integer) -> Integer getNumberOfPaths (x, y) = snd $ memoize (const Nothing) getNumberOfPaths' (x, y) getNumberOfPaths' :: ((Integer, Integer) -> Maybe Integer) -> (Integer, Integer) -> ((Integer, Integer) -> Maybe Integer, Integer) getNumberOfPaths' mem (0,_) = (mem, 1) getNumberOfPaths' mem (_,0) = (mem, 1) getNumberOfPaths' mem (x,y) = (mem'', first + second) where (mem', first) = memoize mem getNumberOfPaths' (x-1, y) (mem'', second) = memoize mem' getNumberOfPaths' (x, y-1) memoize :: Eq a => (a -> Maybe b) -> ((a-> Maybe b) -> a -> ((a -> Maybe b), b)) -> a -> ((a -> Maybe b), b) memoize mem f x = case mem x of (Just y) -> (mem, y) Nothing -> (mem'', y) where (mem', y) = f mem x mem'' = \x' -> if x' == x then Just y else mem' x' ...

mem

我想知道你是否想要a)使用地图来存储值,以及b)传递一个函数State。但是,我怀疑这会很棘手,因为,当你可以传递一个从地图中提取并返回提取的值的函数时,你不能从这个函数中提取地图以插入一些东西地图。

还可以为此创建一个monad(或使用import { Component } from "@angular/core"; @Component({ selector: "mrdb-app", templateUrl: "./Scripts/app/app.component.html" }) export class AppComponent { pageTitle: string = "Movies Review Database"; } )。但是,这可能会留给另一个答案。

答案 4 :(得分:0)

  

我想要的是一个通用的memoize函数,它允许递归调用,就像getNumberOfPaths的定义一样,其中计算逻辑不必完全关心memoization的完成方式。

状态monad非常适合处理状态更新,例如更新到memoized值的映射,而不必在代码的“业务逻辑”部分中明确传递它,如{}的另一个答案{3}}。

在将记忆的细节与递归函数分开方面,您可以隐藏在type后面使用地图甚至状态的事实。递归函数的所有定义需要知道它必须返回MyMemo a b,而不是直接调用自身,它必须传递自己和myMemo的下一个参数

import qualified Data.Map as Map
import Control.Monad.State.Strict

main = print $ runMyMemo getNumberOfPaths (20, 20)

getNumberOfPaths :: (Integer, Integer) -> MyMemo (Integer, Integer) Integer
getNumberOfPaths (0, _) = return 1
getNumberOfPaths (_, 0) = return 1
getNumberOfPaths (x, y) = do
  n1 <- myMemo getNumberOfPaths (x-1,y)
  n2 <- myMemo getNumberOfPaths (x,y-1)
  return (n1 + n2)

-------

type MyMemo a b = State (Map.Map a b) b

myMemo :: Ord a => (a -> MyMemo a b) -> a -> MyMemo a b
myMemo f x = gets (Map.lookup x) >>= maybe y' return
  where
    y' = do
      y <- f x
      modify $ Map.insert x y
      return y

runMyMemo :: Ord a => (a -> MyMemo a b) -> a -> b
runMyMemo f x = evalState (f x) Map.empty

以上内容实际上是https://stackoverflow.com/a/44492608/1319998的滚动版本(在国家之上滚动)。

感谢https://stackoverflow.com/a/44478219/1319998myMemo

中代码的建议