【问题标题】:Why does my Haskell program never print to the console?为什么我的 Haskell 程序从不打印到控制台?
【发布时间】:2014-01-26 23:22:22
【问题描述】:

我想练习在 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"

【问题讨论】:

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


    【解决方案1】:

    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 动作

    【讨论】:

    • 非常感谢您的设计见解。我让它大部分工作。当 not 使用 threadDelay 来减慢输出时,它看起来很糟糕。当使用threadDelay 时,输出被阻塞,直到发送SIGTERM
    • @awashburn 也许你可以用你的threadDelay 问题打开另一个问题。
    • 确保您没有使用缓冲句柄 - 或为每一帧调用 hFlush
    【解决方案2】:

    作为一个粗略的一般规则:IO 值通常应该只出现在函数箭头的右侧1。我不知道你已经阅读了多少关于 monads 的内容... 最好提一下 Haskell 对 monads 所做的事情更多的是Kleisli arrows,所以典型的签名是A -&gt; M B 的形式,带有“纯”AB

    现在这并不能真正回答你的问题,但是如果你相应地重构你的程序(我想你还是想要练习)我怀疑它会起作用,所以我会这样离开它;你的代码对我来说有点太宽泛了,我负担不起详细阅读它的时间......


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

    【讨论】:

      【解决方案3】:

      go 获取一个从未评估过的IO 操作列表,因为您从未要求它们进行评估。 maybeAddToQueue 也一样。相反,您可能希望随时评估它们。

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

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

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

      先修改draw:

      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 操作。

      【讨论】:

        猜你喜欢
        • 2012-11-01
        • 2018-04-15
        • 2019-08-15
        • 2016-02-02
        • 2020-06-05
        • 2011-11-22
        • 2015-08-16
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多