计数器每次都被初始化?

时间:2011-11-21 22:10:45

标签: haskell

我试着制作一个简单的计数器。然而,我的柜台并没有上升。在我看来好像每次都通过函数“inc”重新初始化,或者(n + 1)不起作用。我该如何最好地解决这个问题?

inc :: Int -> IO Int
inc n = return (n+1)

main :: IO ()
main = do
  let c = 0
  let f = 0
  putStrLn "Starting..."
  conn <- connect "192.168.35.62" 8081
  time $
    forM_ [0..10000] $ \i -> do
      p <- ping conn "ping"
      if p=="pong" then inc c
         else inc f
  printf "Roundtrips %d\n" (c::Int)

4 个答案:

答案 0 :(得分:21)

虽然可变变量可以在Haskell中使用,如其他评论者所示,但它不是一个好的风格:在大多数情况下不应该使用变异。

inc函数按值接受其参数,也就是说,它不会修改其参数。此外,let声明的变量保留其初始值,因此您无法更改它们。

如果不能更改任何变量,您如何写?答案是:

  1. 而不是就地修改某些内容,返回一个新值
  2. for循环,使用递归
  3. 幸运的是,您很少需要自己编写递归,因为大多数递归模式已经在标准库中。

    在您的情况下,您需要执行多个IO操作并返回两个计数器的最终值。让我们从一个动作开始:

    let tryOnePing (c, f) i = do
        p <- ping conn "ping"
        return $ if p == "pong" then (c+1, f) else (c, f+1)
    

    这里我们声明一个带有2个参数的本地函数:计数器的当前值,打包在元组(Int, Int)(其他语言中的结构)和当前迭代Int中。该函数执行IO操作并返回计数器IO (Int, Int)的修改值。这一切都在其类型中表示:

    tryOnePing :: (Int, Int) -> Int -> IO (Int, Int)
    

    ping返回IO String类型的值。要进行比较,您需要String而不需要IO。为此,您应该使用>>=函数:

    let tryOnePing (c, f) i = ping conn "ping" >>= \p -> {process the string somehow}
    

    由于这种模式很常见,因此可以像这样编写

    let tryOnePing (c, f) i = do
        p <- ping conn "ping"
        {process the string somehow}
    

    但意思完全相同(编译器将do表示法转换为>>=的应用程序。

    处理显示了一些更常见的模式:

    if p == "pong" then (c+1, f) else (c, f+1)
    

    此处if不是命令式if,而更像是其他语言中的三元condition ? value1 : value2运算符。另请注意,我们的tryOnePing函数接受(c,f)并返回(c+1, f)(c, f+1)。我们使用元组,因为我们只需要使用2个计数器。如果计数器数量很多,我们需要声明一个结构类型并使用命名字段。

    整个If构造的值是一个元组(Int,Int)。 ping是一个IO操作,因此tryOnePing也必须是IO操作。 return函数不是强制性返回,而是将(Int, Int)转换为IO (Int, Int)的方法。

    所以,因为我们有tryOnePing,我们需要编写一个循环来运行它1000次。你的forM_不是一个好的选择:

    1. 它不会在迭代之间传递我们的两个计数器
    2. _表示它会抛出计数器的最终值而不是返回它
    3. 此处您不需要forM_,而是foldM

      foldM tryOnePing (0, 0) [0 .. 10000]
      

      foldM执行由列表的每个元素参数化的IO操作,并在迭代之间传递一些状态,在我们的示例中是两个计数器。它接受初始状态,并返回最终状态。当然,当它执行IO操作时,它返回IO(Int,Int),因此我们需要使用>>=再次提取它以显示:

      foldM tryOnePing (0, 0) [0 .. 10000] >>= \(c, f) -> print (c, f)
      

      在Haskell中,您可以执行所谓的“eta reduction”,即可以从函数声明的两侧删除相同的标识符。例如。 \foo -> bar foobar相同。所以在这种情况下>>=你可以写:

      foldM tryOnePing (0, 0) [0 .. 10000] >>= print
      

      do符号短得多:

       do
         (c, f) <- foldM tryOnePing (0, 0) [0 .. 10000]
         print (c, f)
      

      另请注意,您不需要有两个计数器:如果您有3000个成功,那么您有7000个失败。所以代码变成了:

      main = do
          conn <- connect "192.168.35.62" 8081
          let tryOnePing c i = do
              p <- ping conn "ping"
              return $ if p == "pong" then c+1 else c
          c <- foldM tryOnePing 0 [0 .. 10000]
          print (c, 10000 - c)
      

      最后,在Haskell中,将IO操作与非IO代码分开是很好的。因此,最好将ping中的所有结果收集到列表中,然后计算成功的ping:

      main = do
          conn <- connect "192.168.35.62" 8081
          let tryOnePing i = ping conn "ping"
          pings <- mapM tryOnePing [0 .. 10000]
          let c = length $ filter (\ping -> ping == "pong") pings
          print (c, 10000 - c)
      

      请注意,我们完全避免增加。

      它写得更短,但需要更多的阅读和写作技巧。别担心,你很快就会学到这些技巧:

      main = do
          conn <- connect "192.168.35.62" 8081
          c <- fmap (length . filter (== "pong")) $ mapM (const $ ping conn "ping") [0 .. 10000]
          print (c, 10000 - c)
      

答案 1 :(得分:7)

在Haskell中,默认情况下数据是不可变的。这意味着c中的inc c始终为零。

要在Haskell中获取可变变量,您必须明确地要求它们 ,即使用IORefs。使用它们你可以写出类似的东西:

import Data.IORef

inc :: IORef Int -> IO ()
inc ref = modifyIORef ref (+1)

main :: IO ()
main = do
  c <- newIORef 0
  f <- newIORef 0
  putStrLn "Starting..."
  conn <- connect "192.168.35.62" 8081
  time $
    forM_ [0..10000] $ \i -> do
      p <- ping conn "ping"
      if p=="pong" 
         then inc c
         else inc f
  c' <- readIORef c
  printf "Roundtrips %d\n" c'

答案 2 :(得分:5)

就像在IO之外的代码中一样,您可以使用折叠将一系列计算串联起来。 foldM在monad中运行,例如

main = do
    conn <- connect "192.168.35.62" 8081
    let tryOnePing (c, f) i = do
        p <- ping conn "ping"
        return $ if p == "pong" then (c+1, f) else (c, f+1)
    (c, f) <- foldM tryOnePing (0, 0) [0 .. 10000]
    print (c, f)

答案 3 :(得分:2)

变量在Haskell中是不可变的。当您致电inc f时,它会返回您会立即忽略的0 + 1值。 f的值为0,并且将一直保持这样。