turion / rhine

Haskell Functional Reactive Programming framework with type-level clocks
http://hackage.haskell.org/package/rhine
121 stars 21 forks source link

Unexpected behavior, with effects on stdin/out not running #153

Closed freckletonj closed 3 years ago

freckletonj commented 3 years ago

I've spent some time boiling this bug (?) down to a simple example:

  1. readLn
  2. (*100)
  3. print
input :: ClSF IO StdinClock () Int
input = arrMCl (const $ fromMaybe 0 . readMaybe <$> getLine)

stepFn :: Monad m => ClSF m (Millisecond 300) Int Int
stepFn = arr (*100)

output :: Show a => ClSF IO (Millisecond 500) a ()
output = arrMCl print

main :: IO ()
main = flow $
  (input @@ StdinClock)
  >-- (keepLast 1 -@- concurrently) -->
  (stepFn @@ waitClock)
  >-- (collect -@- concurrently) -->
  (output @@ waitClock)

When I run this, it starts printing as expected.

Then I enter a number at the repl, and printing stops.

If I enter numbers 4 times printing continues, but the interim 3 numbers fall into a black hole, never getting processed.

Enter a 5th number, and printing stops again, until you've entered 4 more numbers.

Each time you get printing to resume, it catch-up-prints dozens of numbers, but they'll all be the most recently input number, many times all instantaneously.

EDIT: I thought it was a flushing issue, but nope, that number never gets processed, as evident in the "catchup" prints. I've even tried passing stdin/out handles throughout and manually flushing, and... no go.

EDIT: Doesn't help to print to stderr, nor to change the buffering status of the handle (hSetBuffering stdin NoBuffering). Probably just spinning my wheels at this point :sweat_smile:

EDIT: Let me know if you struggle to reproduce it, I can screencast a video.

turion commented 3 years ago

This is to be expected, in fact. I ought to document this better. Maybe a FAQ section in the README.md, or what do you think?

The reason is that you have a blocking ClSF, namely input, in the signal network. All purely data-related components should be non-blocking, only clocks are allowed to block. There is no obvious way to prevent blocking IO.

We can simplify your program to make it work. The standard approach to deal with blocking IO is to put it in a clock. In the case of stdin, this has already been done in form of the StdinClock, as you've found out. Since the clock already does the blocking IO, you don't need to do it again with getLine. Instead, you can simply use tagS to retrieve the input line. It should look something like:

input :: ClSF IO StdinClock () Int
input = tagS >-> arr (readMaybe >>> fromMaybe 0)

(You can use >>> instead of >-> as well, matter of taste)

freckletonj commented 3 years ago

put blocking code in a clock

ohhh, that makes so much more sense and works perfectly. I'll add a couple of my recent questions and your answers to a FAQ sometime today :D