Почему моя программа на Haskell никогда не выводит на консоль?

Я хотел попрактиковаться в использовании монады IO в Haskell, поэтому я решил сделать программу-"экранную заставку", которая будет бесконечно повторяться при выводе на консоль. Когда код запускается, в консоли ничего не появляется. Когда я отправляю 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"

person recursion.ninja    schedule 26.01.2014    source источник


Ответы (3)


Эта строка в функции go:

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

на самом деле не выполняет никаких действий ввода-вывода — он просто создает новые действия ввода-вывода из существующих.

Вот некоторые сигнатуры того, как я подхожу к проблеме:

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 однако необходимо использовать генератор случайных чисел, так что это действие ввода-вывода
person ErikR    schedule 27.01.2014
comment
Большое спасибо за ваши дизайнерские идеи. Я заставил его работать в основном. Когда не используется threadDelay для замедления вывода, это выглядит ужасно. При использовании threadDelay вывод блокируется до тех пор, пока не будет отправлено SIGTERM. - person recursion.ninja; 27.01.2014
comment
@awashburn Возможно, вы могли бы открыть еще один вопрос со своей проблемой threadDelay. - person Daniel Wagner; 27.01.2014
comment
убедитесь, что вы не используете буферизованный дескриптор, или вызывайте hFlush для каждого кадра - person ErikR; 27.01.2014

Грубое общее правило: значения IO обычно должны появляться только справа от функциональных стрелок1. Я не знаю, сколько вы уже читали о монадах... было бы неплохо упомянуть, что Haskell делает с монадами более стрелки Клейсли, чем что-либо еще, поэтому типичная подпись имеет форму A -> M B с "чистыми" A и B.

Это на самом деле не отвечает на ваш вопрос сейчас, но если вы соответствующим образом реорганизуете свою программу (я полагаю, вы все равно хотите попрактиковаться), я подозреваю, что это сработает, поэтому я оставлю это так; ваш код слишком обширен для меня, чтобы позволить себе подробно изучить его...


1Конечно, есть исключения из этого правила, на самом деле некоторые очень важные — универсальные комбинаторы действий, циклы и т. д. Но их немного, и они уже определены в стандартном модуле Control.Monad.

person leftaroundabout    schedule 26.01.2014

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))

У Draw нет причин брать 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 никогда не выполнялось.

person user2407038    schedule 27.01.2014