【问题标题】:how to implement a loop with conditional breaks in Haskell如何在 Haskell 中实现带有条件中断的循环
【发布时间】:2017-10-20 17:34:11
【问题描述】:

我想找出满足 的第一个n。如果我使用另一种语言,例如 c/c++,这是一件简单而容易的事情,但我不知道如何在 Haskell 中实现它。

#include <iostream>
long double term(int k) { return 1.0/(k*k+2.0*k); }
int main() {
    long double total = 0.0;
    for (int k=1;;k++) {
        total += term(k);
        if (total>=2.99/4.0) {
            std::cout << k << std::endl;
            break;
        }
    }
    return 0;
}

我将 dropWhile 与有序列表一起使用,并取 1 来提取第一个。

term k = 1.0/(k*k+2.0*k)
termSum n = sum $ take n $ map term [1..]
main = do
  let [(n,val)] = take 1 $ dropWhile (\(a,b)->b <= 2.99/4.0) $ map (\n->(n,termSum n)) [1..]
  print n

我知道这很可怕。写这个的最好和直观的方法是什么?

回复: 感谢您的精彩回答!使用修复功能的那个似乎是我机器中最快的(Redhat 6.4 64bit / 80GB 内存)

method#0 take 1 and dropWhile(我的初始实现)

threshold=0.74999         n=99999     time=52.167 sec

方法#1 使用修复功能

threshold=0.74999         n=99999     time=0.005 sec
threshold=0.74999999      n=101554197 time=1.077 sec
threshold=0.7499999936263 n=134217004 time=1.407 sec

方法#2 倒退

threshold=0.74999         n=99999     time=0.026 sec
threshold=0.74999999      n=101554197 time=21.523 sec
threshold=0.7499999936263 n=134217004 time=25.247 sec

method#3 命令式方式

threshold=0.74999         n=99999     time=0.008 sec
threshold=0.74999999      n=101554197 time=2.460 sec
threshold=0.7499999936263 n=134217004 time=3.254 sec

重来: 我注意到无论我使用什么实现方式(修复、命令式或递归方式),如果阈值大于 0.7499999936264 ......它永远不会结束......为了让 f(n) 大于 0.7499999936264,我认为我们只是由于 ![f(n)=\frac_{3n^2+5n}^{4n^2+12n+8}] 需要计算高达 150,000,000 的项。我使用 Integer 而不是 Int,但它也没有帮助。如果我将阈值设置为大于 0.7499999936264 ...,是否有任何原因无法完成?

【问题讨论】:

  • 我可能会用显式递归来编写它。我认为这里看起来很干净。

标签: haskell


【解决方案1】:

我喜欢在这种情况下倒退:

main = print k where
  k = 1 + length (takeWhile (< (2.99/4)) partialSums)
  partialSums = scanl1 (+) terms
  terms = [ 1.0/(k*k+2.0*k) | k <- [1..] ] 

这是如何工作的:

terms 是一个无限列表,但由于 Haskell 是惰性的,我们只会根据需要计算每个术语:

λ terms = [ 1.0/(k*k+2.0*k) | k <- [1..] ] :: [Double]
λ take 5 terms
[0.3333333333333333,0.125,6.666666666666667e-2,4.1666666666666664e-2,2.857142857142857e-2]
λ :p terms
terms = 0.3333333333333333 : 0.125 : 6.666666666666667e-2 :
        4.1666666666666664e-2 : 2.857142857142857e-2 : (_t5::[Double])

partialSums 是另一个无限列表,基于terms 的内容(使用scanl1)。它可以让我们摊销您在计算 termSum 时所做的工作:

λ partialSums = scanl1 (+) terms
λ take 5 partialSums 
[0.3333333333333333,0.4583333333333333,0.525,0.5666666666666667,0.5952380952380952]

takeWhile (&lt; (2.99/4)) 然后确定我们需要生成多少个partialSums 的术语,从而确定多少个术语 terms我们需要生成:

λ length (takeWhile (< (2.99/4)) partialSums)
398

如果我们检查,我们可以看到前 398 个 terms 的总和小于 2.99 / 4,但第 399 个却超过了:

λ sum (take 398 terms) < 2.99/4
True
λ sum (take 399 terms) < 2.99/4
False

或者,相当于第 397 个部分和(从 0 开始的索引)小于目标,而第 398 个不是:

λ partialSums !! 397 < 2.99/4
True
λ partialSums !! 398 < 2.99/4
False

【讨论】:

  • 次要问题:您需要k+1,而不是k,因为您要删除术语大于或等于2.99/4 的第一个元素。
【解决方案2】:

本质上是显式递归,但我喜欢 fix 这样的循环:

import Data.Function (fix)

term k = 1.0 / (k*k+2.0*k)

main = print $ fix (\f total k ->
                      let new = total + term k
                      in if new >= 2.99/4.0 then k else f new (k+1)
                   ) 0 1

【讨论】:

  • fix 是做什么的?
【解决方案3】:

如果我使用另一种语言,例如 c/c++,这将是一件简单而容易的事情

那么,让我们以同样的方式来做吧。

import Prelude hiding (break)
import Data.IORef
import Control.Monad.Cont
import Data.Foldable
import Control.Monad (when)

-- long double term(int k) { return 1.0/(k*k+2.0*k); }
term :: Int -> Double
term k = 1.0/(k'*k'+2.0*k')
   where k' = fromIntegral k

-- int main() {
main :: IO ()
main = flip runContT return $ do
   -- long double total = 0.0;
   total <- lift $ newIORef (0.0 :: Double)
   -- for (int k=1;;k++) {
   callCC $ \break ->
      for_ [1..] $ \k -> do
         -- total += term(k);
         lift $ modifyIORef total (+ term k)
         -- if (total>=2.99/4.0) {
         totalV <- lift $ readIORef total
         when (totalV >= 2.99/4.0) $ do
            -- std::cout << k << std::endl;
            lift $ print k
            -- break;
            break ()

是的,以上内容与其说是严肃的回答,不如说是一个玩笑。不过,很高兴看到,至少在理论上,用 Haskell 编写命令式代码是可能的。

它只会导致非惯用的 Haskell,它并不比原始代码更难读或写。毕竟,朋友之间的callCC 或两个是什么? :-P

【讨论】:

  • 对于好奇的:延续基本上是 FP 的 goto 版本。 callCC 引入了一个“标签”;它将一个延续传递给它的参数,当使用某个参数x 调用(“goto”)该延续时,控制跳转到callCC 之后,并返回x 的值。
  • 这是一个很好的笑话。但是这种与IORef 的混战似乎是低效的矫枉过正。为什么不将ContTState 一起使用?哦,我想这会妨碍妙语中的print....
  • @dfeuer 我同意。我需要print 的 IO。我想我本可以使用ContT+StateT+IO。 Refs 确实是矫枉过正,但我​​想展示一种非常通用(即使不是非常优雅)的方式来做到这一点。
猜你喜欢
  • 1970-01-01
  • 2021-12-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-06-26
相关资源
最近更新 更多