使用break-s / continue-s将命令控制流转换为haskell

时间:2010-12-22 18:47:24

标签: haskell functional-programming imperative-programming

考虑以下命令性代码,找到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风格的案件的方法是什么?

9 个答案:

答案 0 :(得分:7)

Daniel's和sepp2k的类似答案:

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

有关更多解释和示例,请参阅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

答案 1 :(得分:5)

如果我们取消所有优化并将所有数字组合乘以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 ..])

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

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

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

答案 2 :(得分:2)

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

f = [999,998..100]

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

for循环对应于不同的功能概念,具体取决于您在每次迭代中所做的事情。有时map是适当的类比,有时是fold,有时是其他类似物。通常情况下,这是事物的组合。在这种情况下,您可以有效地组合两个列表。在Haskell中实现这一点的一种方法是列表理解:

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

此处g表示先前定义的序列的每个元素与其自身组合的乘积列表。换句话说,几乎就是你在for循环中发生的事情。

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

答案 3 :(得分:2)

这里没有一个通用的答案。但让我们来看看这个具体的例子:

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

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

答案 4 :(得分:2)

所以,从功能上思考,你应该在寻找解决问题的方法,而不是循环和步骤,而不是功能。

因此,如果我们有一个函数maxWhere f xs返回xf x为真的最大maxpal = maxWhere pal [x * y | x <- [999,998..100], y <- [999,998..100]] ,我们可以写:

maxWhere f xs = maximum $ filter f xs

maxWhere的天真实现是

f

但如果maxWhere f xs = foldl' r 0 xs where r a x | x > a = if f x then x else a | otherwise = a 比比较贵,那么这很糟糕,因为我们会对f进行更多的调用。我们可以使用fold将过滤器和最大值组合成一个通道,并获得与命令式代码相同的行为。

(*) <$> [999,998..100] <*> [999,998..100]

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

(我真的想拼写候选号码列表{{1}},但这可能会引入不必要的并发症。)

答案 5 :(得分:1)

尔加。被sepp2k击败,但我会回答你的一般问题:

临时变量也可以使用状态monad或ST monad表示,如果你有很多。 FP经常在简洁和清晰度方面获胜,但在某些情况下它不会,例如当有几个局部变量来处理时。

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

答案 6 :(得分:1)

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

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

我们可以写下像这样的isPalindrome:

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解决方案中,你还将我们到目前为止找到的最大数字除以当前的x(x*y => 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) < curr实际上只是意味着从现在开始,[x*x,(x-1)*x..curr]将是空的。正如您所看到的,python代码中的中断强制执行的所有边界都适合x上的一次迭代(使用递归)和x * y值列表中的find。它可能看起来不那么好,但在我看来,我更明确地说明了我们对x和y的限制。

运行它我们得到:

*Main> search 999 0
906609

事实证明,因为906609的平方根是952而在x * x < curr时停止是个好主意...

答案 7 :(得分:0)

正如 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函数参数用于“内循环”以退出一次(i * j&gt; current违反最大条件(由于“内循环”而返回当前最大值)。在这里,我们需要从一个“循环级别”中逃脱,因此这里的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; m)条件)或Nothing(MonadPlus为零)和mplus之后(或Alternatice。&lt; | &gt;)it有效地返回新的最大回文或先前的最大回报(返回m)。由于find在找到第一个满足元素后停止搜索,因此该代码的行为与命令式curmax模拟完全相同。 两个版本都在[99999..10000]范围内运行0.5秒。

<强>更新 只是为了好玩:同样的方法,但使用StateT Integer (Cont Integer) () - Cont从“循环”逃脱,并且状态传递最大回文(加上使用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)  

答案 8 :(得分:0)

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

这是一个更简单的例子(取自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 []){     int 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对应于内循环。