Haskell: 有条件地打破循环

Haskell: Break a loop conditionally

我想在这种情况下打破循环:

import Data.Maybe (fromJust, isJust, Maybe(Just))

tryCombination :: Int -> Int -> Maybe String
tryCombination x y 
               | x * y == 20 = Just "Okay"
               | otherwise = Nothing

result :: [String]
result = map (fromJust) $ 
    filter (isJust) [tryCombination x y | x <- [1..5], y <- [1..5]]

main = putStrLn $ unlines $result

想象一下,"tryCombination" 比这个例子要复杂得多。而且它消耗了大量 cpu 电量。这不是对 25 种可能性的评估,而是 26^3。

因此,当 "tryCombination" 找到给定组合的解决方案时,它 returns 为 Just,否则为 Nothing。如何在第一个找到的解决方案上立即打破循环?

简单的解决方案:findjoin

您似乎在寻找 Data.List.findfind 具有类型签名

find :: (a -> Bool) -> [a] -> Maybe a

所以你会做类似的事情

result :: Maybe (Maybe String)
result = find isJust [tryCombination x y | x <- [1..5], y <- [1..5]]

或者,如果您不想要 Maybe (Maybe String)(为什么要这样做?),您可以将它们与带有签名

Control.Monad.join 折叠在一起
join :: Maybe (Maybe a) -> Maybe a

所以你有

result :: Maybe String
result = join $ find isJust [tryCombination x y | x <- [1..5], y <- [1..5]]

更高级的解决方案:asum

如果您想要更高级的解决方案,可以使用 Data.Foldable.asum,它具有签名

asum :: [Maybe a] -> Maybe a

它的作用是从众多列表中挑选出第一个 Just 值。它通过使用 MaybeAlternative 实例来实现。 MaybeAlternative 实例是这样工作的:(导入 Control.Applicative 以访问 <|> 运算符)

λ> Nothing <|> Nothing
Nothing
λ> Nothing <|> Just "world"
Just "world"
λ> Just "hello" <|> Just "world"
Just "hello"

换句话说,它从两个选项中选择第一个 Just 值。想象一下将 <|> 放在列表的每个元素之间,这样

[Nothing, Nothing, Just "okay", Nothing, Nothing, Nothing, Just "okay"]

转向

Nothing <|> Nothing <|> Just "okay" <|> Nothing <|> Nothing <|> Nothing <|> Just "okay"

这正是 asum 函数的作用!由于 <|> 短路,它只会评估第一个 Just 值。有了它,您的功能将像

一样简单
result :: Maybe String
result = asum [tryCombination x y | x <- [1..5], y <- [1..5]]

您为什么需要这个更高级的解决方案?它不仅更短;一旦您了解了惯用语(即,当您熟悉 Alternativeasum 时),只需阅读代码的前几个字符,就会更加清楚该函数的作用。

要回答您的问题,您需要 find 函数。获得 Maybe (Maybe String) 后,您可以将其转换为 Maybe String with join

虽然 find 更好、更易读并且肯定只做需要的事情,但我不太确定您在问题中遇到的代码效率低下。懒惰的评估可能会处理这个问题并只计算需要的东西,(仍然可以消耗额外的内存)。有兴趣的可以试试benchmark。

您正在寻找的评估策略正是 MonadPlus. In particular, there is the function msumMaybe 实例的目的,其类型专用于这种情况

msum :: [Maybe a] -> Maybe a

直觉上,这个版本的 msum 获取可能失败的计算列表,一个接一个地执行它们,直到第一个计算成功并 returns 相应的结果。所以,result 会变成

result :: Maybe String
result = msum [tryCombination x y | x <- [1..5], y <- [1..5]]

最重要的是,您可以通过从 Maybe 泛化到 MonadPlus:

的任何实例,使您的代码在某种意义上与确切的评估策略无关
tryCombination :: MonadPlus m => Int -> Int -> m (Int,Int)
-- For the sake of illustration I changed to a more verbose result than "Okay".
tryCombination x y 
    | x * y == 20 = return (x,y) -- `return` specializes to `Just`.
    | otherwise   = mzero        -- `mzero` specializes to `Nothing`.

result :: MonadPlus m => m (Int,Int)
result = msum [tryCombination x y | x <- [1..5], y <- [1..5]]

要获得您想要的行为,只需 运行 以下内容:

*Main> result :: Maybe (Int,Int)
Just (4,5)

但是,如果您决定不仅需要第一个组合,还需要所有组合,只需使用 MonadPlus:

[] 实例
*Main> result :: [(Int,Int)]
[(4,5),(5,4)]

我希望这在概念层面上有所帮助,而不仅仅是提供解决方案。

PS:我刚刚注意到 MonadPlusmsum 对于这个目的确实有点太严格了,Alternativeasum 本来可以够了。

在这种情况下,懒惰实际上可以解决这个问题。

通过调用 unlines,您正在请求 all 您的 "loop"1 的输出,所以很明显第一次成功后就停不下来了tryCombination。但是如果你只需要一个匹配,只需使用 listToMaybe (来自 Data.Maybe);如果根本没有匹配项,它会将您的列表转换为 Nothing,或者 Just 找到第一个匹配项。

惰性意味着列表中的结果只会按需评估;如果您不再要求列表中的任何更多元素,则生成它们所需的计算(甚至查看列表中是否有 个元素)将永远不会是 运行!

这意味着您通常不必像在命令式语言中那样"break loops"。您可以将完整的 "loop" 编写为列表生成器,消费者可以独立决定他们想要多少。这个想法的极端情况是 Haskell 非常乐意生成甚至过滤 infinite 列表;它只会 运行 生成代码刚好足以生成与您稍后最终检查的元素一样多的元素。


1 实际上,即使 unlines 也会产生惰性字符串,所以如果您只阅读生成的连接字符串的第一行,您可以 still "break the loop" 尽早!但是你在这里打印整个东西。