英文:
Haskell: cache result of a function in pattern matching
问题
You can improve your code by using memoization to cache the results of searchHelper
calls. Here's a modified version of your code that incorporates memoization using a helper function and a data structure to store cached results:
import Data.Map (Map)
import qualified Data.Map as Map
data Tree a = Empty | Node a (Tree a) (Tree a)
deriving (Show, Eq)
data Step = StepL | StepR
deriving (Show, Eq)
-- Define a memoization data structure for caching results
type MemoMap a = Map (a, Tree a, [Step]) (Maybe [Step])
-- The main search function
search :: Eq a => a -> Tree a -> Maybe [Step]
search targetValue root = searchHelper targetValue root []
-- The memoized search helper function
searchHelper :: Eq a => a -> Tree a -> [Step] -> Maybe [Step]
searchHelper targetValue Empty _ = Nothing
searchHelper targetValue (Node nodeValue leftChild rightChild) stepsSoFar =
case Map.lookup (targetValue, (Node nodeValue leftChild rightChild), stepsSoFar) memo of
Just result -> result
Nothing ->
let leftResult = searchHelper targetValue leftChild (stepsSoFar ++ [StepL])
rightResult = searchHelper targetValue rightChild (stepsSoFar ++ [StepR])
result =
if targetValue == nodeValue
then Just stepsSoFar
else if leftResult /= Nothing then leftResult
else rightResult
in (Map.insert (targetValue, (Node nodeValue leftChild rightChild), stepsSoFar) result memo, result)
where
memo = Map.empty
main :: IO ()
main = do
let tree = Node 1 (Node 2 Empty Empty) (Node 3 Empty Empty)
print $ search 2 tree
This code uses a MemoMap
data structure to cache the results of previous searchHelper
calls based on the target value, tree structure, and steps taken so far. This way, you avoid redundant calculations and improve the efficiency of your search function.
英文:
I have the following algebraic data type:
data Tree a = Empty | Node a (Tree a) (Tree a)
deriving (Show, Eq)
Also, I have
data Step = StepL | StepR
deriving (Show, Eq)
Now, I need a function search
that takes
- a root of the tree
- a target value
t
... and it must return a path of type[Step]
leading to a node with valuet
. Also, ift
is not present in the tree,search
must returnNothing
. Finally, the input is guaranteed to have the target value at most once.
My best effort, as of now, is:
searchHelper :: Eq a => a -> Tree a -> [Step] -> Maybe [Step]
searchHelper _ Empty _ = Nothing
searchHelper targetValue (Node nodeValue leftChild rightChild) stepsSoFar =
if targetValue == nodeValue then Just stepsSoFar
else if searchHelper targetValue leftChild (stepsSoFar ++ [StepL]) /= Nothing then searchHelper targetValue leftChild (stepsSoFar ++ [StepL])
else if searchHelper targetValue rightChild (stepsSoFar ++ [StepR]) /= Nothing then searchHelper targetValue rightChild (stepsSoFar ++ [StepR])
else Nothing
search :: Eq a => a -> Tree a -> Maybe [Step]
search targetValue root = searchHelper targetValue root []
As you can see, I call the searchHelper
too often (else if searchHelper targetValue leftChild (stepsSoFar ++ [StepL]) /= Nothing then searchHelper targetValue leftChild (stepsSoFar ++ [StepL])
). I need a machinery that would allow me to cache the results of searchHelper
calls and use them
in if ... then ... else
.
Q: How can I do it?
答案1
得分: 3
使用单词cache让我感到困惑,但如果我正确理解问题,真正的问题是重复使用相同的表达式。在更大的代码库中,这肯定会成为可读性和可维护性问题,因此值得解决。
从上下文来看,这似乎是一个"玩具问题"。这没什么问题 - 我自己玩很多这样的问题来学习新东西。我提到它的原因是从这个和其他线索中,我了解到你仍然是Haskell的初学者。再次强调:这没什么问题,但这意味着我会跳过一些稍微高级的Haskell内容。
像在原帖中那样检查Nothing
或Just
很少是Haskell的惯用写法。相反,你会使用模式匹配或(更常见的是)一些用于处理Maybe
的高级API(如Functor
、Applicative
、Monad
等)。
话虽如此,我了解到这可能不是你现在需要的。为了减少表达式的重复,你可以在Haskell中使用let..in
语法:
searchHelper :: Eq a => a -> Tree a -> [Step] -> Maybe [Step]
searchHelper _ Empty _ = Nothing
searchHelper targetValue (Node nodeValue leftChild rightChild) stepsSoFar =
if targetValue == nodeValue then Just stepsSoFar
else
let l = searchHelper targetValue leftChild (stepsSoFar ++ [StepL])
in if l /= Nothing then l
else
let r = searchHelper targetValue rightChild (stepsSoFar ++ [StepR])
in if r /= Nothing then r
else Nothing
这使你可以'声明' '变量' l
和 r
并重复使用它们。
正如我漫长的序言所示,这仍然不是Haskell的惯用写法,但我希望它能解决你的直接问题。
英文:
The use of the word cache confused me, but if I understand the question correctly, the real problem is the repeated use of the same expression. That could certainly become a readability and maintainability issue in a larger code base, so is worthwhile addressing.
From the context this looks like a 'toy problem'. There's nothing wrong with that - I play with plenty of those myself to learn new stuff. The reason I mention it, though, is that from this and other clues I gather that you're still a Haskell beginner. Again: nothing wrong with that, but it just means that I'm going to skip some of the slightly more advanced Haskell stuff.
Checking for Nothing
or Just
like in the OP is rarely idiomatic Haskell. Instead you'd use pattern-matching or (more commonly) some of the higher-level APIs for working with Maybe
(such as Functor
, Applicative
, Monad
, etc.).
That said, I gather that this isn't quite what you need right now. In order to cut down on the duplication of expressions, you can use let..in
syntax in Haskell:
searchHelper :: Eq a => a -> Tree a -> [Step] -> Maybe [Step]
searchHelper _ Empty _ = Nothing
searchHelper targetValue (Node nodeValue leftChild rightChild) stepsSoFar =
if targetValue == nodeValue then Just stepsSoFar
else
let l = searchHelper targetValue leftChild (stepsSoFar ++ [StepL])
in if l /= Nothing then l
else
let r = searchHelper targetValue rightChild (stepsSoFar ++ [StepR])
in if r /= Nothing then r
else Nothing
This enables you to 'declare' 'variables' l
and r
and reuse them.
As my lengthy preamble suggests, this still isn't idiomatic Haskell, but I hope it adresses the immediate question.
答案2
得分: 1
I'd like to propose a dissenting answer. The answer to the general question is indeed to use let
(or where
) to name the computation, and refer to that name twice. But the answer to this specific question is to dissolve the problem. Instead of:
if {- X -} /= Nothing then {- X -} else {- Y -}
use a case
to inspect X
and dispatch on its value. Like this:
case {- X -} of
Nothing -> {- Y -}
Just v -> Just v
This Just v -> Just v
is very readable, but it does have an unfortunate operational interpretation: destructure the Maybe
value, then immediately build a new Maybe
value that's exactly the same. Rather than allocating a fresh value, you can simply return the one you've got using as-patterns: the pattern var@pat
(where var
is a variable name and pat
is a pattern) matches exactly when pat
matches, but also binds the name var
to the entire value that was matched. So:
case {- X -} of
Nothing -> {- Y -}
v@(Just _) -> v
If we were to apply this pattern of rewrite to your original code, here's where we'd land:
searchHelper targetValue (Node nodeValue leftChild rightChild) stepsSoFar =
if targetValue == nodeValue then Just stepsSoFar
else case searchHelper targetValue leftChild (stepsSoFar ++ [StepL]) of
Nothing -> case searchHelper targetValue rightChild (stepsSoFar ++ [StepR]) of
Nothing -> Nothing
v@(Just _) -> v
v@(Just _) -> v
This rewrite reveals something quite nice: the inside case
always returns the exact thing it sees! So we can simply return the exact thing it sees directly, without ever inspecting it with case
.
searchHelper targetValue (Node nodeValue leftChild rightChild) stepsSoFar =
if targetValue == nodeValue then Just stepsSoFar
else case searchHelper targetValue leftChild (stepsSoFar ++ [StepL]) of
Nothing -> searchHelper targetValue rightChild (stepsSoFar ++ [StepR])
v@(Just _) -> v
This would be a fine spot for a beginner to stop. For an expert, I would expect at this point to notice that the case
used above is an instance of the (<|>)
function, defined so for Maybe
values:
Nothing <|> r = r
l <|> _ = l
So:
searchHelper targetValue (Node nodeValue leftChild rightChild) stepsSoFar =
if targetValue == nodeValue then Just stepsSoFar
else searchHelper targetValue leftChild (stepsSoFar ++ [StepL])
<|> searchHelper targetValue rightChild (stepsSoFar ++ [StepR])
In this particular case, I think I would also consider converting the first part of the if
to a bare Maybe
value so that I could chain <|>
s. That would give you one of these (asum
is the listified form of chaining <|>
s):
-- the empty is just to make the following lines more uniform, it should be compiled away
searchHelper targetValue (Node nodeValue leftChild rightChild) stepsSoFar = empty
<|> (stepsSoFar <$ guard (targetValue == nodeValue))
<|> searchHelper targetValue leftChild (stepsSoFar ++ [StepL])
<|> searchHelper targetValue rightChild (stepsSoFar ++ [StepR])
-- OR:
searchHelper targetValue (Node nodeValue leftChild rightChild) stepsSoFar = asum
[ stepsSoFar <$ guard (targetValue == nodeValue)
, searchHelper targetValue leftChild (stepsSoFar ++ [StepL])
, searchHelper targetValue rightChild (stepsSoFar ++ [StepR])
]
This fully addresses your original concern. To be really thorough with my suggestions, I'd also want to see stepsSoFar
get built up in reverse; repeatedly prepending to a list is much cheaper than repeatedly appending.
searchHelper targetValue (Node nodeValue leftChild rightChild) stepsSoFar = asum
[ reverse stepsSoFar <$ guard (targetValue == nodeValue)
, searchHelper targetValue leftChild (StepL : stepsSoFar)
, searchHelper targetValue rightChild (StepR : stepsSoFar)
]
At this point, I would consider this quite idiomatic Haskell. Whether the repetition in the final two elements of the asum
bothers you is an aesthetic choice that I could see two experts disagreeing on. If it bothers you, it could be addressed like this:
searchHelper targetValue (Node nodeValue leftChild rightChild) stepsSoFar =
(reverse stepsSoFar <$ guard (targetValue == nodeValue))
<|>
[ searchHelper targetValue child (step : stepsSoFar)
| (child, step) <- [(leftChild, StepL), (rightChild, StepR)]
]
Personally I find the additional complexity not worth the sharing, so I would skip this change. (Actually, to be completely fair, I would probably make this change, look at it, and then revert it. ^_^)
英文:
I'd like to propose a dissenting answer. The answer to the general question is indeed to use let
(or where
) to name the computation, and refer to that name twice. But the answer to this specific question is to dissolve the problem. Instead of:
if {- X -} /= Nothing then {- X -} else {- Y -}
use a case
to inspect X
and dispatch on its value. Like this:
case {- X -} of
Nothing -> {- Y -}
Just v -> Just v
This Just v -> Just v
is very readable, but it does have an unfortunate operational interpretation: destructure the Maybe
value, then immediately build a new Maybe
value that's exactly the same. Rather than allocating a fresh value, you can simply return the one you've got using as-patterns: the pattern var@pat
(where var
is a variable name and pat
is a pattern) matches exactly when pat
matches, but also binds the name var
to the entire value that was matched. So:
case {- X -} of
Nothing -> {- Y -}
v@(Just _) -> v
If we were to apply this pattern of rewrite to your original code, here's where we'd land:
searchHelper targetValue (Node nodeValue leftChild rightChild) stepsSoFar =
if targetValue == nodeValue then Just stepsSoFar
else case searchHelper targetValue leftChild (stepsSoFar ++ [StepL]) of
Nothing -> case searchHelper targetValue rightChild (stepsSoFar ++ [StepR]) of
Nothing -> Nothing
v@(Just _) -> v
v@(Just _) -> v
This rewrite reveals something quite nice: the inside case
always returns the exact thing it sees! So we can simply return the exact thing it sees directly, without ever inspecting it with case
.
searchHelper targetValue (Node nodeValue leftChild rightChild) stepsSoFar =
if targetValue == nodeValue then Just stepsSoFar
else case searchHelper targetValue leftChild (stepsSoFar ++ [StepL]) of
Nothing -> searchHelper targetValue rightChild (stepsSoFar ++ [StepR])
v@(Just _) -> v
This would be a fine spot for a beginner to stop. For an expert, I would expect at this point to notice that the case
used above is an instance of the (<|>)
function, defined so for Maybe
values:
Nothing <|> r = r
l <|> _ = l
So:
searchHelper targetValue (Node nodeValue leftChild rightChild) stepsSoFar =
if targetValue == nodeValue then Just stepsSoFar
else searchHelper targetValue leftChild (stepsSoFar ++ [StepL])
<|> searchHelper targetValue rightChild (stepsSoFar ++ [StepR])
In this particular case, I think I would also consider converting the first part of the if
to a bare Maybe
value so that I could chain <|>
s. That would give you one of these (asum
is the listified form of chaining <|>
s):
-- the empty is just to make the following lines more uniform, it should be compiled away
searchHelper targetValue (Node nodeValue leftChild rightChild) stepsSoFar = empty
<|> (stepsSoFar <$ guard (targetValue == nodeValue))
<|> searchHelper targetValue leftChild (stepsSoFar ++ [StepL])
<|> searchHelper targetValue rightChild (stepsSoFar ++ [StepR])
-- OR:
searchHelper targetValue (Node nodeValue leftChild rightChild) stepsSoFar = asum
[ stepsSoFar <$ guard (targetValue == nodeValue)
, searchHelper targetValue leftChild (stepsSoFar ++ [StepL])
, searchHelper targetValue rightChild (stepsSoFar ++ [StepR])
]
This fully addresses your original concern. To be really thorough with my suggestions, I'd also want to see stepsSoFar
get built up in reverse; repeatedly prepending to a list is much cheaper than repeatedly appending.
searchHelper targetValue (Node nodeValue leftChild rightChild) stepsSoFar = asum
[ reverse stepsSoFar <$ guard (targetValue == nodeValue)
, searchHelper targetValue leftChild (StepL : stepsSoFar)
, searchHelper targetValue rightChild (StepR : stepsSoFar)
]
At this point, I would consider this quite idiomatic Haskell. Whether the repetition in the final two elements of the asum
bothers you is an aesthetic choice that I could see two experts disagreeing on. If it bothers you, it could be addressed like this:
searchHelper targetValue (Node nodeValue leftChild rightChild) stepsSoFar =
(reverse stepsSoFar <$ guard (targetValue == nodeValue))
<|>
[ searchHelper targetValue child (step : stepsSoFar)
| (child, step) <- [(leftChild, StepL), (rightChild, StepR)]
]
Personally I find the additional complexity not worth the sharing, so I would skip this change. (Actually, to be completely fair, I would probably make this change, look at it, and then revert it. ^_^)
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论