我想练习在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"
答案 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 B
和A
。
现在这并没有真正回答你的问题,但是如果你相应地重构你的程序(我想你无论如何都想要练习)我怀疑它会起作用,所以我会这样离开它;你的代码对我来说有点过于宽泛,需要花时间详细介绍它......
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
个动作被运行。