我对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'
调用mem
(x'
)。 mem
只返回y''
,其中y''
包含在再次调用memoize'
的结果中。 memoize'
还会返回一个新地图,然后我们会向其添加新值并将其用作memoize'
的返回值。
此处的问题是,(y'', map') = memoize' map func' x''
行应位于mem
下,因为它取决于x''
,mem
是map'
的参数。我当然可以这样做,但之后我将失去Map
值,这是我需要的,因为它包含来自中间计算的记忆值。但是,我不想将mem
引入memoize
的返回值,因为传递给Map
的函数必须处理memoize
。< / p>
很抱歉,如果这听起来很混乱。很多这种超高阶功能的东西让我感到困惑。
我确信有办法做到这一点。我想要的是一个通用的getNumberOfPaths
函数,它允许递归调用,就像在{{1}}的定义中一样,计算逻辑不必完全关心memoization是如何完成的。
答案 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的滚动版本(在国家之上滚动)。
中代码的建议