【问题标题】:Translate imperative control flow with break-s/continue-s to haskell使用 break-s/continue-s 将命令式控制流转换为 haskell
【发布时间】:2011-05-29 14:12:47
【问题描述】:

考虑以下命令式代码,它在 3 位数字的乘积中找到最大的回文(是的,这是“[18 世纪杰出数学家] 项目”网站的第一个任务):

curmax = 0
for i in range(999,100):
for j in range(999,100):
    if ((i*j) < curmax): break
    if (pal(i*j)):
        curmax = i*j
        break
print curmax

当我目前正在学习 Haskell 时,我的问题是,你如何翻译这个(以及基本上任何包含比简单迭代更复杂的东西的命令式构造,例如中断、继续、临时变量以及所有这些)到 Haskell?

我的版本是

maxpal i curmax
    | i < 100 = curmax
    | otherwise = maxpal (i-1) (innerloop 999)
    where 
        innerloop j
            | (j < 100) || (p < curmax) = curmax
            | pal p = p
            | otherwise = innerloop (j-1)
            where p = i*j
main = print $ maxpal 999 0

但这看起来我们仍然处于势在必行的丑陋之城。

那么你有什么建议,FP风格的处理这种情况的方法是什么?

【问题讨论】:

  • 好吧,如果您尝试将命令式控制流转换为 Haskell,那么您进入命令式丑陋小镇,几乎按照定义。
  • 这才是真正的问题:不是如何盲目地将命令式代码转换为 Haskell 语法,而是在函数式编程中使用什么方法来优雅地处理复杂的流程。
  • 这可能有点格格不入,但我通常不会从命令式转换为函数式,而是返回规范(规范通常不关心控制流)并从那里开始工作。当您确实有复杂的控制流时,延续传递风格 (CPS) 和惰性求值(可以将生产与中间值的消费分开)是抽象复杂性的两种有价值的技术。

标签: haskell functional-programming imperative-programming


【解决方案1】:

我认为你可以使用两个相互递归的函数来做你想做的事。

这是一个更简单的例子(取自a tutorial on ATS):

implement main (argc, argv) = let
  fun loop1 (i: int): void =
    if i <= 9 then loop2 (i, i) else ()

  and loop2  (i: int, j: int): void =
    if j <= 9 then begin
      if i < j then begin
        print ", ";
        print "("; print i; print ", "; print j; print ")";
        loop2 (i, j+1)
      end
    end else begin
      print_newline ();
      loop1 (i+1)
    end
  in
    loop1 0
  end

上面写的代码和你用 C 写的很像(取自同一页):

int main (int argc, char *argv[]) { 整数 i, j ;

for (i = 0; i <= 9; i += 1) {
  for (j = i; j <= 9; j += 1) {
    if (i < j) printf (", ") ; printf ("(%i, %i)", i, j) ;
  } /* for */
  printf ("\n") ;
} /* for */

return 0 ;

}

如您所见,嵌套循环变成了相互递归的函数;可变变量 i 和 j 成为归纳变量。 loop1 对应外循环,loop2 对应内循环。

【讨论】:

    【解决方案2】:

    这种循环很容易用于列表理解,如下所示:

    maximum [x*y | x <- [999..100], y <- [999..100],isPalindrome (x*y)]
    

    我们可以这样写回文:

    isPalindrome x = xs == reverse xs
      where xs = show x
    

    这确实足够快,虽然有点不聪明,所以首先我们会注意到我们检查了两次数字。假设 a*b 是最大的回文,那么我们将检查 x == a, y==bx==b, y==a 的情况。因此,首先我们通过将搜索的数字限制为仅 x >= y 的情况来阻止这种情况,如下所示:

    maximum [x*y | x <- [999..100], y <- [x..100],isPalindrome (x*y)]
    

    这将要测试的数字减半。

    在您的 python 解决方案中,您还将 y 限制为我们迄今为止发现的最大数字除以当前 x (x*y =&gt; curmax),而且您永远不会搜索超过找到的第一个 y(如果 curmax 更新,则打破内部循环)。如果我们检查的第一个元素(x 平方)小于我们当前的答案,我们可以通过不继续进一步减少搜索,因为所有后续检查都更小,但这超出了列表理解中看起来不错的范围,因此我们将搜索移至它自己的功能:

    import Data.List(find)
    import Data.Maybe(isNothing,fromJust)
    
    search x curr 
       | x * x < curr                   = curr
       | isNothing maypal || pal < curr = search (x - 1) curr 
       | otherwise                      = search (x - 1) pal 
       where maypal = find isPalindrome [x * x, (x - 1) * x .. curr]
             pal    = fromJust maypal
    

    值得注意的是,我们的限制,(x*x) &lt; curr,实际上只是意味着从现在开始,[x*x,(x-1)*x..curr] 将是空的。正如您所看到的,所有由 python 代码中的中断强制执行的边界都适合 x 的一次迭代(使用递归)和 x*y 值列表的查找。它可能看起来不太好,但在我看来,我们更明确地说明了我们对 x 和 y 所做的限制。

    运行它我们得到:

    *Main> search 999 0
    906609
    

    事实证明,在x * x &lt; curr 时停止是一个非常好的主意,因为 906609 的平方根是 952...

    【讨论】:

      【解决方案3】:

      正如 stephen tetley 在他的评论中所指出的,在 FP 中,您可以使用延续传递样式来处理复杂的控制流(Cont monad 加上它的 callCC,这在某种程度上类似于 break . ...甚至goto - 滥用 CPS 会导致相当难以理解的代码 - 请参阅下面的示例):

      import Control.Monad.Cont
      
      pal n = sn == reverse sn
          where sn = show n
      
      range = [99999,99998..10000]
      
      mfoldM a r f = foldM f a r  
      
      curmaxm = (`runCont` id) $ mfoldM 0 range $ \m i ->
                  callCC $ \break ->
                      mfoldM m range $ \m j -> do
                        let ij = i*j
                        if ij < m
                           then break m
                           else return $
                                if pal ij then ij else m
      

      两个 mfoldM(只是一个标准的 foldM,其参数重新排列)对应于原始样本中的两个循环,break function-argument 用于“内部循环”以退出一次(i*j > current max)条件被违反(由于“内循环”而返回当前最大值)。这里我们只需要从一个“循环级别”中逃脱,所以这里的 callCC 绝对是大材小用。

      同样的逻辑也可以用find(+ Haskell 的懒惰)实现:

      import Data.List
      import Data.Maybe
      import Control.Monad
      
      curmax = fromJust $ foldM it 0 range
          where 
            it m i = (find pal . takeWhile (>m) . map (*i) $ range) `mplus` return m
      

      find pal 此处返回第一个回文数(也将满足 takeWhile 中的 (>m) 条件)或 Nothing(MonadPlus 的零)和 mplus 之后(或 Alternatice.)it 有效返回一个新的最大回文数或前一个最大值(返回 m)。由于find 一旦找到第一个满足的元素就停止搜索,因此此代码的行为与其命令式curmax 模拟完全相同。 两个版本都在 0.5 秒内运行 [99999..10000] 范围。

      更新: 只是为了好玩:相同的方法,但使用StateT Integer (Cont Integer) () - 继续从“循环”和状态中逃脱以传递最大回文(加上使用forM_when 的能力)。相同的效率:

      import Control.Monad.Cont
      import Control.Monad.State.Strict
      
      solcs = runCont (execStateT comp 0) id
          where   
            comp = forM_ range $ \i -> callCC $ \break ->
                      forM_ range $ \j -> do
                        let ij = i*j
                        m <- get
                        when (ij < m) (break ())
                        when (pal ij) (put ij)  
      

      【讨论】:

        【解决方案4】:

        因此,从功能上考虑,您应该寻找将问题分解为函数而不是循环和步骤的方法。

        所以,如果我们有一个函数maxWhere f xs,它返回最大的x,而f x 为真,我们可以这样写:

        maxpal = maxWhere pal [x * y | x <- [999,998..100], y <- [999,998..100]]
        

        maxWhere 的简单实现是

        maxWhere f xs = maximum $ filter f xs
        

        但是如果f 比比较更昂贵,这很糟糕,因为我们将比原来调用更多的 f。我们可以使用 fold 将过滤器和最大值组合成单遍,并获得与命令式代码相同的行为。

        maxWhere f xs = foldl' r 0 xs
            where r a x
               | x > a     = if f x then x else a
               | otherwise = a
        

        在这里使用零作为一个神奇的小数是可怕的,但在这种情况下有效。

        (我真的很想拼写那个候选号码列表(*) &lt;$&gt; [999,998..100] &lt;*&gt; [999,998..100],但这可能会在这里引入不必要的复杂性。)

        【讨论】:

          【解决方案5】:

          这里没有万能的答案。但是让我们看一下这个具体的例子:

          首先,考虑外部循环:我们总是做完整的范围,我们只关心最终的最大值,所以这很容易:

          outerLoop = foldl innerLoop 0 [999,998..100]
          

          在内部循环中,我们有一些 i 的值和一个当前最大值。现在我们只关心 i*j 大于当前最大值的范围:

          innerLoop curmax i = foldr checkMax curmax [999*i, 998*i .. curmax]
          

          在核心逻辑中,我们得到一个 i*j 的值,我们知道它总是大于或等于当前最大值,所以只需检查下一个值是否是回文:如果是,我们完成了,因为序列减少了。如果没有,请推迟决定:

          checkMax ij defer = if pal ij then ij else defer
          

          【讨论】:

          • +1 我是 Haskell 新手,但我很惊讶这个答案没有投票,显然它很好地模仿了 python 代码的意图,而且它仍然清晰紧凑。一个小建议,我认为 [ii, (i-1)*i .. curmax] 在第二行就足够了(所以你不要同时检查 ab 和 b*a) .
          • @tokland:这听起来是对的,但正如您所注意到的,我在模仿 Python 版本的概念(而不是句法)结构,所以我将保持代码不变。不过观察力不错!
          【解决方案6】:

          呸。被 sepp2k 打败了,但我会回答你的一般性问题:

          临时变量也可以使用 state monad 来表示,如果有很多的话,也可以使用 ST monad。 FP 通常以简洁明了的方式取胜,但在某些情况下并非如此,例如当有多个局部变量需要处理时。

          惰性可以模拟很多中断,但是在处理 IO 时,通常必须使用显式递归。但是,'List' 包(来自 Hackage)在允许您以函数式样式编写 IO 循环方面非常聪明。

          【讨论】:

            【解决方案7】:

            如果我们取消所有优化,只乘以 100 到 999 之间的所有数字组合,过滤掉非回文并取其中的最大值,我们可以非常简洁地将函数编写为:

            maximum $ filter pal [x*y | x <- [100..999], y <- [100..999]]
            

            当然这基本上是效率最低的方法,但是由于数量相对较少,所以在我的机器上仍然可以在半秒内完成。

            但是,如果我们想要在算法上更符合您的 python 解决方案的路线,我们可以这样做:

            import Data.Maybe
            import Data.List
            
            maxpal i curmax
                | i < 100 = curmax
                | otherwise = maxpal (i-1) newmax
                where newmax = fromMaybe curmax (find pal bigger)
                      bigger = takeWhile (> curmax) (map (*i) [999, 998 ..])
            

            这里的外循环与您的解决方案基本相同,但我们使用列表函数替换了内循环。

            我们使用map (*i) [999, 998, ...] 为从999 倒计时的每个j 创建产品i*j。使用takeWhile,我们说一旦值不大于curmax,列表就应该停止。

            然后我们使用find 来查看该列表中的任何项目是否是回文。如果是,列表中的第一个回文数就是我们的新最大值。如果不是,我们保留旧的最大值。 (find 返回 MaybefromMaybe 采用默认值和 Maybe 并返回来自 Maybe 的值或如果 Maybe 中没有值则返回默认值)

            【讨论】:

            • 您的第二个解决方案正是我想要的。它突出了几个迭代原语,如 find 和 takeWhile,这对像我这样的新手很有好处,并且完全符合我的命令式代码的作用。
            • 令我吃惊的是,每个人都开始建议列表理解(我应该将其添加到问题中——我知道)。在这种情况下它显然不能很好地工作:将“3-digit numbers”约束更改为 4-digit,你将永远等待。在这种特殊情况下,无论多么聪明的理解,你都无法逃避 O(n^2)。
            • @dorseg:关键是在这种情况下,输入大小是固定的,对于给定的输入大小,列表理解工作得相当好。如果您知道输入大小永远不会增长,则没有理由进行优化。第一个解决方案的问题也不在于它是一个列表理解,而是它是二次的。 map (*i) [999, 998 ..] 也可以写成列表理解 ([x*i| x &lt;- [999, 998 ..]]) 而不会损失任何性能。
            【解决方案8】:

            Daniel 和 sepp2k 的类似回答:

            惰性函数式编程使您可以以比您在问题中的命令式控制流中看到的更加模块化的方式编写程序。 例如,形成因子 999...100 的列表,然后是所有产品,然后过滤以仅保留回文,然后计算最大值。 多亏了惰性,这些中间列表只会在需要的时候生成,并且会被逐步回收。

            更多解释和示例,请参阅 John Hughes 的经典论文Why Functional Programming Matters

            maxpal :: Int
            maxpal = maximum [i*j | i <- factors, j <- factors, pal (i*j) ]
            
            factors :: [Int]
            factors = [999,998..100]
            
            pal :: Show a => a -> Bool
            pal = palL . show
            
            palL :: (Eq a) => [a] -> Bool
            palL xs = xs == reverse xs
            

            【讨论】:

            • 嗯。 dorserg 的版本首先测试产品大小,然后然后尝试回文测试,这可能是 Daniel、sepp2k 和我的(都非常相似的)建议中缺少的重要优化。
            • 实际上我的第二个更长的解决方案并没有丢失它。
            • @sepp2k:很抱歉在我的比较中没有更具体。到目前为止给出的优雅和可组合版本中缺少优化,包括你的。我想知道在进行这种优化的同时如何优雅地和模块化地编写 maxpal。
            • 我觉得列表理解+过滤是一种工具,在大多数情况下产生两个或多个源的笛卡尔积,并不是真正的“生产代码”。 IE。太慢了——有点像玩具。这是危险的,因为语法非常简洁。我希望扩展列表推导以指定应如何执行搜索。如果程序员无法指定应该如何执行搜索,那么它可能真的很慢。
            • @user239558:您是否有任何证据支持您对列表理解和过滤执行缓慢的感觉?您是否知道在编译时消除了(去糖)推导?您是否阅读过有关融合框架优化的论文和统计数据?那里有很多信息和经验。
            【解决方案9】:

            在我看来,范围对应于列表。例如:

            f = [999,998..100]
            

            现在f被定义为从999到100的数字序列。

            for 循环对应于不同的功能概念,具体取决于您在每次迭代中所做的事情。有时map 是适当的模拟,有时是fold,有时是别的。很多时候,它是事物的组合。在这种情况下,您有效地组合了两个列表。在 Haskell 中做到这一点的一种方法是列表推导:

            g = [(x * y) | x <- f , y <- f]
            

            这里g 表示先前定义的序列的每个元素与其自身组合的乘积列表。换句话说,您在 for 循环中所做的几乎都是如此。

            从这里开始,您可能希望 filter 生成的序列仅包含回文值,然后从该集合中计算 maximum 值。

            【讨论】:

              猜你喜欢
              • 2015-01-25
              • 2019-08-22
              • 2011-12-31
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2016-03-10
              • 1970-01-01
              相关资源
              最近更新 更多