缓存函数在模式匹配中的结果。

huangapple go评论52阅读模式
英文:

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

  1. a root of the tree
  2. a target value t
    ... and it must return a path of type [Step] leading to a node with value t. Also, if t is not present in the tree, search must return Nothing. 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内容。

像在原帖中那样检查NothingJust很少是Haskell的惯用写法。相反,你会使用模式匹配或(更常见的是)一些用于处理Maybe的高级API(如FunctorApplicativeMonad等)。

话虽如此,我了解到这可能不是你现在需要的。为了减少表达式的重复,你可以在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

这使你可以'声明' '变量' lr 并重复使用它们。

正如我漫长的序言所示,这仍然不是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])
   &lt;|&gt; 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 &lt;|&gt;s. That would give you one of these (asum is the listified form of chaining &lt;|&gt;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
  &lt;|&gt; (stepsSoFar &lt;$ guard (targetValue == nodeValue))
  &lt;|&gt; searchHelper targetValue leftChild (stepsSoFar ++ [StepL])
  &lt;|&gt; searchHelper targetValue rightChild (stepsSoFar ++ [StepR])

-- OR:
searchHelper targetValue (Node nodeValue leftChild rightChild) stepsSoFar = asum
  [ stepsSoFar &lt;$ 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 &lt;$ 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 &lt;$ guard (targetValue == nodeValue))
  &lt;|&gt;
    [ searchHelper targetValue child (step : stepsSoFar)
    | (child, step) &lt;- [(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 -&gt; {- Y -}
    Just v -&gt; Just v

This Just v -&gt; 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 -&gt; {- Y -}
    v@(Just _) -&gt; 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 -&gt; case searchHelper targetValue rightChild (stepsSoFar ++ [StepR]) of
           Nothing -&gt; Nothing
           v@(Just _) -&gt; v
         v@(Just _) -&gt; 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 -&gt; searchHelper targetValue rightChild (stepsSoFar ++ [StepR])
         v@(Just _) -&gt; 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 (&lt;|&gt;) function, defined so for Maybe values:

Nothing &lt;|&gt; r = r
l       &lt;|&gt; _ = l

So:

searchHelper targetValue (Node nodeValue leftChild rightChild) stepsSoFar = 
  if targetValue == nodeValue then Just stepsSoFar 
  else searchHelper targetValue leftChild (stepsSoFar ++ [StepL])
   &lt;|&gt; 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 &lt;|&gt;s. That would give you one of these (asum is the listified form of chaining &lt;|&gt;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
  &lt;|&gt; (stepsSoFar &lt;$ guard (targetValue == nodeValue))
  &lt;|&gt; searchHelper targetValue leftChild (stepsSoFar ++ [StepL])
  &lt;|&gt; searchHelper targetValue rightChild (stepsSoFar ++ [StepR])

-- OR:
searchHelper targetValue (Node nodeValue leftChild rightChild) stepsSoFar = asum
  [ stepsSoFar &lt;$ 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 &lt;$ 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 &lt;$ guard (targetValue == nodeValue))
  &lt;|&gt;
    [ searchHelper targetValue child (step : stepsSoFar)
    | (child, step) &lt;- [(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. ^_^)

huangapple
  • 本文由 发表于 2023年6月1日 12:26:51
  • 转载请务必保留本文链接:https://go.coder-hub.com/76378657.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定