8

The timeout function in System.Timeout sometimes fails to halt an infinite computation.

For example,

timeout 1000 $ print $ length [0..]

returns Nothing as expected because the timeout interrupts the infinite computation. But

timeout 1000 $ print $ length $ cycle [1,2,3]

loops forever.

This is on a Mac, using ghc or ghci 8.6.4.

I expect the second example to behave like the first, interrupting the infinite computation after 1 millisecond and returning Nothing. Instead, the second example hangs.

Will Ness
  • 70,110
  • 9
  • 98
  • 181
  • 7
    Yes, unfortunately, tight loops that don't allocate memory can't be interrupted with `timeout`. GHC pretends it does preemptive threading, and the facade is very good, but in fact within each OS thread it does cooperative threading. Allocation -- usually a very frequent event in immutable languages -- is the trigger for choosing whether to yield control or not. But GHC is smart enough to be able to optimize this loop into one that doesn't allocate, by observing that `length`'s internal counter can be updated in-place (and `cycle [1,2,3]` allocates as much as it needs in time, it seems). – Daniel Wagner Mar 25 '19 at 13:14
  • 1
    See the first entry of [Bugs in GHC](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/bugs.html#bugs-in-ghc). It's a long standing issue, which is not easy to fix without disabling a lot of optimizations. – chi Mar 25 '19 at 14:09
  • 2
    @chi Nice! If -fno-omit-yields as suggested in your link works, it seems like it's worth writing a (short) answer suggesting it. – Daniel Wagner Mar 25 '19 at 15:03
  • @DanielWagner It seems rather fragile to me, and it's module-wide instead of expression-wide. Also, Will Ness below found a duplicate, I think. – chi Mar 25 '19 at 15:43
  • Hm. Seems related, though it focuses on interactive, rather than programmatic, ways of stopping infinite loops. – Daniel Wagner Mar 25 '19 at 15:48

1 Answers1

4

You can use your own, non-sharing implementation of cycle:

> _Y g = g (_Y g)
_Y :: (t -> t) -> t

> take 10 . _Y $ ([1,2,3] ++)
[1,2,3,1,2,3,1,2,3,1]
it :: Num a => [a]

> timeout 100000 . print . length . _Y $ ([1,2,3] ++)
Nothing
it :: Maybe ()
(0.11 secs, 152470624 bytes)

_Y will of course allocate an infinite, growing list, unlike the sharing cycle which is equivalent to fix ([1,2,3] ++) which creates an actual cyclic list in memory:

> timeout 100000 . print . length . fix $ ([1,2,3] ++)
<<<hangs>>>

See also:

Will Ness
  • 70,110
  • 9
  • 98
  • 181