为什么我的Haskell程序永远不会打印到控制台?

时间:2014-01-26 23:22:22

标签: haskell console infinite-loop lazy-evaluation console-output

我想练习在Haskell中使用IO monad,所以我决定制作一个"屏幕保护程序"打印到控制台时无限递归的程序。当代码运行时,控制台上不会显示任何内容。当我将SIGTERM发送到该程序时,它会打印出硬编码的概念证明' draw输出但没有来自无限递归(go函数)的输出。

我怀疑这与懒惰评估有关,在go函数中输出到控制台的代码永远不会被调用,但我不知道如何修复它。任何建议将不胜感激!

Haskell代码:

import Data.Maybe (isJust, fromJust)
import System.Random
import System.Console.ANSI
import qualified System.Console.Terminal.Size as Term

data RainDrop a = RainDrop
  { row   :: !a
  , col   :: !a
  , count :: !a
  } deriving (Read,Show)

main :: IO ()
main = do
  clearScreen
  -- proof that draw works
  c <- applyX 10 draw (return (RainDrop 0 2 10))
  go [return (RainDrop 0 0 10)]

applyX :: Int -> (a -> a) -> a -> a
applyX 0 _ x = x
applyX n f x = applyX (n-1) f (f x)

go :: [IO (RainDrop Int)] -> IO ()
go []     = return ()
go (x:xs) = do
  prng <- newStdGen
  go $ map draw $ maybeAddToQueue prng (x:xs)

maybeAddToQueue :: RandomGen g => g -> [IO (RainDrop Int)] -> [IO (RainDrop Int)]
maybeAddToQueue _    []     = []
maybeAddToQueue prng (x:xs) =
  let
    (noNewDrop, gen0) = randomR (True,False) prng
  in
    if noNewDrop
    then x:xs
    else (
      do
        (colR,gen1) <- randomCol gen0
        return $ RainDrop 0 colR $ fst $ randomLen gen1
      ):x:xs

randomCol :: RandomGen g => g -> IO (Int, g)
randomCol prng = do
  w <- Term.size >>= (\x -> return . Term.width  $ fromJust x)
  return $ randomR (0,(w-1)) prng

randomLen :: RandomGen g => g -> (Int, g)
randomLen = randomR (4,32)

draw :: IO (RainDrop Int) -> IO (RainDrop Int)
draw rain = do
  x    <- rain
  prng <- newStdGen
  setCursorPosition (row x) (col x)
  putChar . toGlyph $ fst $ randomR range prng
  return (RainDrop (succ $ row x) (col x) (count x))

toGlyph x
 | isJust a  = fromJust a
 | otherwise = x
 where a = lookup x dictionary

dictionary =
  let (a,b) = range
  in zip [a..b] encoding

encoding =
  let (a,b) = splitAt 16 katakana
      (c,d) = splitAt 7  b
  in a ++ numbers ++ c ++ ['A'..'Z'] ++ d

range    = (' ','~')
katakana = ['・'..'゚']
numbers  = "012Ƹ߈Ƽ6ߖȣ9"

3 个答案:

答案 0 :(得分:4)

go函数中的这一行:

go $ map draw $ maybeAddToQueue prng (x:xs)

实际上并不执行任何IO操作 - 它只是从现有IO操作创建新的IO操作。

以下是我将如何解决问题的一些类型签名:

type World = [Raindrop]

-- draw the raindrops
draw :: World -> IO ()

-- advance the drops
step :: World -> World

-- add more drops
moreRain :: World -> IO (World)

-- the main loop
loop :: World -> IO ()
loop drops = do
  draw drops
  let drops' = step drops
  drops'' <- moreRain drops'
  -- delay for a while here???
  loop drops''

注意:

  • 我假设step是一个纯粹的函数,假设滴的运动是确定性的
  • moreRain但是需要使用随机数生成器,因此它是一个IO动作

答案 1 :(得分:1)

作为粗略的一般规则:IO值通常只应出现在函数箭头 1 的右侧。我不知道你已经阅读了多少关于monad的内容......可能很高兴提到Haskell对monad的处理比Kleisli arrows更多,所以典型的签名是{{3}的形式1}},包含“纯”A -> M BA

现在这并没有真正回答你的问题,但是如果你相应地重构你的程序(我想你无论如何都想要练习)我怀疑它会起作用,所以我会这样离开它;你的代码对我来说有点过于宽泛,需要花时间详细介绍它......


1 这个规则当然有例外,实际上是一些非常重要的例子 - 通用动作组合器,循环等等。但是那些很少并且已经在标准模块中定义{{1} }。

答案 2 :(得分:0)

go会获取一系列永不评估的IO操作,因为您从不要求他们这样做。 maybeAddToQueue也是如此。相反,你可能想要随时评估它们。

你正构建一个应该做某事的无限循环。你可以把它归结为forever someAction

此外,您正在IO中执行所有操作,因此您可以使用随机函数的IO版本:

randomLen :: IO Int
randomLen = randomRIO (4,32)

首先修改绘图:

draw :: RainDrop Int -> IO (RainDrop Int)
draw x = do
  setCursorPosition (row x) (col x)
  g <- randomRIO range
  putChar $ toGlyph g
  return (RainDrop (succ $ row x) (col x) (count x))

没有理由抽签IO RainDrop,因为无论如何你都会立即评估它。

maybeAddToQueue :: [RainDrop Int] -> IO [RainDrop Int]
maybeAddToQueue [] = return []
maybeAddToQueue xs = do
  noNewDrop <- randomIO 
  if noNewDrop
  then return xs
  else do
        colR <- randomCol 
        len  <- randomLen 
        return $ (RainDrop 0 colR len):xs

最后,您的go功能:

go :: [RainDrop Int] -> IO ()
go [] = return ()
go a = do
   b <- maybeAddToQueue a
   c <- mapM draw b
   go c

或者,替代版本,它使发生的事情变得更加清晰:

import Control.Monad ((>=>))

go = maybeAddToQueue >=> mapM draw >=> go

请注意map成为mapM。这可确保您的操作确实正在执行。由于永远不需要列表的值,因此简单地使用map将永远不会评估列表中的任何元素,因此没有IO个动作被运行。