IO中的简单计数器

时间:2011-10-19 23:46:05

标签: haskell functional-programming io

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

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

理想情况下,我想按照

的方式做点什么
tick = do putStr (counter)
          counter + 1
    where counter = 0

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

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

这给了我错误:/

感谢任何帮助:)

5 个答案:

答案 0 :(得分:7)

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

y

tick = tick' 0 where ... 行没有做任何事情;它定义了一个从未使用过的名称。 counter = 0函数中使用的counter被绑定为其参数之一(并且阴影定义为0)。花点时间盯着它,看看是否有意义。

我们也可以采用一种很好的“高阶”方式。基本上我们想要运行无限长的代码块:

tick'

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

请注意,这是Haskell中一个非常重要的概念:sequence不打印这三个数字然后构造列表[print 0, print 1, print 2]。相反,它本身就是 actions 的列表,其类型为[0,1,2]。使列表无效;只有当你将一个动作绑定到[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_,以及为此目的而优化的尾部下划线。通常这在Haskell程序中由于垃圾收集无关紧要,但在这个特定的用例中由于各种细微之处而是一种特殊情况。您会发现,如果没有mapM_ s,程序的内存使用量会随着结果列表(在这种情况下为_)构建但从未使用过而逐渐增长。

警告:我已经将[(),(),(),...]sequence的类型签名专门用于mapM,而不是一般的monad,以便读者没有了解同时具有类型和类型类的动作的正交概念。

答案 1 :(得分:3)

好吧,让我们回到基础。你想要什么似乎是一个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

答案 2 :(得分:2)

你的第二次实施非常接近!

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来做到这一点,但所有这一切都是在counter语句中将tick = tick'定义为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>

答案 3 :(得分:1)

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

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

然后你可以用它来创建'滴答'IO函数(带有麻烦:IO这里的函数需要以liftIO为前缀,因为它现在是Ticking a monad不是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

可以使用IO(使用初始计数器值)将其转换为“普通”runStateT

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

所以:

main :: IO ()
main = startTicking ticking 0

答案 4 :(得分:0)

类似于卡尔使用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