【问题标题】:Simple counter in IOIO中的简单计数器
【发布时间】:2011-10-19 23:46:05
【问题描述】:

我正在尝试使用 IO 创建一个无限加 1 的简单计数器。

从那以后我一直在挠头......

理想情况下,我想做一些类似的事情

tick = do putStr (counter)
          counter + 1
    where counter = 0

然后重复这个过程。然后重复前 2 个表达式。或者类似的东西:

tick = tick'
       where 
           counter = 1
           tick' counter | counter > 0 = do putStrLn (show counter)
                                            tick' (counter + 1)
                         | otherwise = tick

这给了我错误:/

任何帮助表示赞赏:)

【问题讨论】:

    标签: haskell functional-programming io


    【解决方案1】:

    有几种方法可以做到这一点使用可变单元格。您已经在第二次尝试中做到了,只是有一点错误。您需要将初始值传递给 tick' 函数,而不是“设置它”(haskell 不知道分配变量 - 只有定义。如果出现 x = y 行,x 将是 y整个生命周期)。

    tick = tick' 0
        where ...
    

    counter = 0 行没有做任何事情;它正在定义一个从未使用过的名称。 tick' 函数中使用的 counter 被绑定为其参数之一(并隐藏定义为 0 的参数)。花点时间考虑一下,看看这是否有意义。

    我们也可以通过一种很好的“高阶”方式来做到这一点。本质上我们想要运行无限长的代码块:

    do
        print 0
        print 1
        print 2
        ...
    

    有一个名为sequence :: [IO a] -> IO [a] 的函数(请参阅下面的警告),它将获取一个动作列表并构造一个动作。因此,如果我们可以构造列表[print 0, print 1, print 2, ...],那么我们可以将其传递给sequence,以构建我们正在寻找的无限长块。

    请注意,这是 Haskell 中一个非常重要的概念:[print 0, print 1, print 2] 不打印这三个数字然后构造列表[0,1,2]。相反,它本身是一个 actions 列表,其类型为 [IO ()]。列出清单什么都不做;只有当您将操作绑定到main 时,它才会被执行。例如,我们可能会说:

    main = do
        let xs = [putStrLn "hello", getLine >> putStrLn "world"]
        xs !! 0
        xs !! 0
        xs !! 1
        xs !! 1
        xs !! 0
    

    这将打印两次hello,两次得到一行并在每次之后打印world,然后再次打印hello

    有了这个概念,很容易用列表理解构建动作列表[print 0, print 1, ...]

    main = sequence [ print x | x <- [0..] ]
    

    我们可以简化一下:

    main = sequence (map (\x -> print x) [0..])
    main = sequence (map print [0..])
    

    所以map print [0..] 是我们正在寻找的[print 0, print 1, ...] 操作列表,然后我们只需将其传递给sequence,后者将它们链接在一起。

    sequence这种模式很常见,并且有自己的mapM

    mapM :: (a -> IO b) -> [a] -> IO [b]
    mapM f xs = sequence (map f xs)
    

    因此:

    main = mapM print [0..]
    

    尽可能简单。

    关于性能的一点说明:由于我们没有使用这些函数的输出,我们应该使用sequence_mapM_,并带有尾随下划线,它们为此目的进行了优化。由于垃圾收集,通常这在 Haskell 程序中无关紧要,但在这个特定的用例中,由于各种微妙之处,这是一种特殊情况。您会发现,如果没有 _s,程序的内存使用量会随着结果列表(在本例中为 [(),(),(),...])的构建而逐渐增加,但从未使用过。

    警告:我已经给出了sequencemapM 的类型签名,专门针对IO,而不是一般的monad,这样读者就不必学习正交概念了同时具有类型和类型类的动作。

    【讨论】:

    • 我回答了一个与您完全不同的问题。据我所知,没有办法像我所做的那样,创建一个每次使用时输出不同内容的单个 IO 操作,而不使用可变单元格。
    • @Carl:这是getStr &gt;&gt;= putStrLn :)
    • @rampion 好的,显然我应该说“跟踪内部状态”。
    • 我喜欢这个方案,用高阶函数逐步解决问题。我很难理解函数序列的真正作用,你的解释让它更清楚了。谢谢你:)
    【解决方案2】:

    好吧,让我们回到基础。你想要的似乎是一个 IO 动作,当绑定时,打印并增加一个计数器?我将从这个假设出发。

    您需要的第一件事是一些可变单元格,因为您每次都使用相同的操作。它需要在里面有一些可变的东西,以便在每次使用时做不同的事情。对于这种情况,我会选择IORef

    但隐藏 IORef 有点棘手。特别是因为全局变量很糟糕。最好的方法是从另一个 IO 操作中创建 IO 操作,然后关闭 IORef。这样做会给你这样的东西:

    import Data.IORef
    
    mkCounter :: IO (IO ())
    mkCounter = do
        ref <- newIORef 0
        return $ do
            counter <- readIORef ref
            print counter
            writeIORef ref $ counter + 1
    

    这可以通过执行以下操作来使用:

    main = do
        tick <- mkCounter
        tick
        tick
        tick
    

    【讨论】:

      【解决方案3】:

      您的第二个实现非常接近!

      tick = tick'
             where 
                 counter = 1
                 tick' counter | counter > 0 = do putStrLn (show counter)
                                                  tick' (counter + 1)
                               | otherwise = tick
      

      让我们看看这个错误:

      Couldn't match expected type `IO b0' with actual type `a0 -> IO b0'
          In the expression: tick'
      

      让我们添加一些类型以确保我们得到我们想要的。

      tick 是一个 IO 操作。我们不在乎动作封装了什么值,因为整个 重点是永远运行。

      tick :: IO a
      

      现在我们的错误是:

      Couldn't match expected type `IO a' with actual type `a0 -> IO b0'
          In the expression: tick'
      

      嗯,这几乎是一样的,没有帮助。让我们继续前进。

      tick' 是一个函数,它接受一些整数并返回一个 IO 操作 打印整数并在下一个值上重复tick'。再说一次,我们不在乎什么 动作封装的值,因为它永远运行。

      tick' :: Int -> IO b
      

      等等,现在这个错误是有道理的!我们定义了tick = tick',但这两个东西有根本不同的类型。一个是动作 (tick) 一个是返回动作的函数 (tick')。我们需要做的就是给tick' 一些值来获取操作,所以让我们这样做。

      您曾尝试通过说where counter = 1 来做到这一点,但所做的只是在声明tick = tick' 中将counter 定义为1,并且由于此处未提及counter,因此未使用它。

      当你说tick' counter | ... = 时,你指的不是上面那行中的counter。在那里,您定义了另一个名为 counter 的变量,该变量仅在 tick' 的定义范围内。

      所以现在我们的代码如下所示:

      tick :: IO a
      tick = tick' 1
             where 
                 tick' :: Int -> IO b
                 tick' counter | counter > 0 = do putStrLn (show counter)
                                                  tick' (counter + 1)
                               | otherwise = tick
      

      如果我们尝试编译它,ghc 不会抱怨,如果我们在 ghci 中尝试它,它会按需要运行:

      % ghci
      ghci> :l Tick.hs
      Ok, modules loaded: Tick.
      ghci> tick
      1
      2
      3
      ...
      25244
      ^C
      Interrupted
      ghci>
      

      【讨论】:

      • 不敢相信我是如此接近。感谢您解释类型错误。
      【解决方案4】:

      对于一个简单的无限计数器,只需使用递归:

      counter n = do print n
                     counter (n+1)
      
      main = counter 1
      

      在不使用可变状态的情况下实现 tick 功能的另一种方法是使用 monad 转换器混合 StateIO monad:

      import Control.Monad.State
      
      type Ticking a = StateT Int IO a
      
      tick :: Ticking ()
      tick = do
           modify succ
           get >>= liftIO . print
      
      getCounterValue :: Ticking Int
      getCounterValue = get
      

      然后你可以用它来创建'ticking'IO函数(麻烦的是:IO这里的函数需要以liftIO为前缀,因为它现在是Ticking amonad而不是IO a):

      ticking :: Ticking ()
      ticking = do
              liftIO $ putStrLn "Starting"
              tick
              tick
              c <- getCounterValue
              liftIO $ do
                  putStrLn ("Finished at " ++ show c)
                  putStrLn "Press any Enter to start infinite counter"
                  getChar
              forever tick
      

      可以使用runStateT(带有初始计数器值)将其转换为“正常”IO

      startTicking :: Ticking a -> Int -> IO a
      startTicking = evalStateT
      

      所以:

      main :: IO ()
      main = startTicking ticking 0
      

      【讨论】:

        【解决方案5】:

        类似于 Carl 使用 STM 的答案的 forkIO 安全版本是

        import Control.Concurrent.STM
        import Control.Monad (replicateM_)
        import Control.Monad(forever)
        
        makeCounter :: IO (IO Int)
        makeCounter = do
          var <- newTVarIO 0
          return $ do
              atomically $ do
                  value <- readTVar var
                  modifyTVar var (+1)
                  readTVar var
        
        -- a version that only counts from 1 to 10
        main1:: IO ()
        main1 = do
          counter <- makeCounter
          replicateM_ 10 $ counter >>= print
        
        
        -- a version that counters forever
        main2 :: IO ()
        main2 = do
          counter <- makeCounter
          forever $ do
              x<- counter
              print x
        
        main :: IO ()
        main = do
          counter <- makeCounter
          tick<- counter
          tick<- counter
          print tick  -- 2
        

        参考:

        1. Mutable closures in Haskell and nested IO

        2. An EXERCISE from STM tutorial

        3. Mutable State in Haskell

        【讨论】:

          猜你喜欢
          • 2012-02-29
          • 1970-01-01
          • 2011-04-15
          • 1970-01-01
          • 1970-01-01
          • 2014-06-12
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多