5

I am experimenting with GHCi's :sprint command. Consider the following:

GHCi> xs = [1..10] :: [Int]
GHCi> :sprint xs
xs = _
GHCi> length xs
10
GHCi> :sprint xs
xs = [1,2,3,4,5,6,7,8,9,10]

This works as expected. What interested me is :sprint's behaviour after we interrupt some computation. Consider the following:

GHCi> xs = [1..] :: [Int]
GHCi> :sprint xs
xs = _
GHCi> length xs
Interrupted.
GHCi> :sprint xs

And it hangs.

The expected result was something like that (modulo the number of :s):

xs = _ : _ : _ : _

What causes :sprint ... to freeze? Why is there no access to information about the part of the list which was computed? It seems like a bug to me - there's no real reason to cast away all the work the interrupted length did. Is it a bug indeed, or am I wrong?

Zhiltsoff Igor
  • 1,812
  • 8
  • 24
  • 7
    I think it didn't freeze. It's just that `length` is much, *much* faster than `:sprint`. Try `length (take 10000 x)` followed by `:sprint`, and compare to how that does with `100000` or `1000000` to see what I mean. Probably by the time you hit ^C, `length` has gone well beyond 1000000 elements. – Daniel Wagner May 06 '21 at 16:44
  • @DanielWagner I must've jumped to conclusions too quickly (quicker than `:sprint` at least). I was used to *regular GHCi output* starting printing the list the moment we ask it to. Why does `:sprint` traverse the whole list first? Can *deeper type constructors* affect the *previous ones*? – Zhiltsoff Igor May 06 '21 at 17:00
  • 3
    For lists specifically? Maybe not. But in general, yes. Define `data List a = Nil | Cons a (List a); data Lol = Lol | Lololololololololololololololololololololololololol` with enough trash at the end to be wider than your terminal. Then compare the `:sprint` output from the fully-evaluated versions of `Cons Lol (Cons Lol Nil)` and `Cons Lol (Cons Lololololololol... Nil)`. The grandchild of the outer `Cons` has decided where the first parenthesis should go. You can cause this decision to be made arbitrarily far away from the parenthesis with a bit of work. – Daniel Wagner May 06 '21 at 17:20
  • @DanielWagner I'm going to be honest - I do not fully understand what you wrote, sorry :P. What does it mean **"The grandchild of the outer Cons has decided where the first parenthesis should go"**? Doesn't the first bracket immediately follow `Cons`'s child? Where else could the parenthesis be? Can you, please elaborate? – Zhiltsoff Igor May 06 '21 at 18:19
  • 1
    Yes, the first bracket follows `Cons`'s child. But whether it's on the same line or a different line is determined by the grandchild! – Daniel Wagner May 06 '21 at 18:33
  • @DanielWagner aha, so that's a problem general for any output, am I right? I. e., if GHCi is trying to output `Lololololol...`, it needs to check whether it fits in the current line first, which also determines the position of parenthesis. Have I finally got that right? – Zhiltsoff Igor May 06 '21 at 20:36
  • I... think so? Not sure. Anyway the short version is that `:sprint` pretty-prints its output, and the pretty-printer's layout algorithm is non-local (it probably uses some dynamic-programming or similar), which means that early parts of the output can depend on (much) later parts. By comparison, derived `Show` instances do not use a pretty-printer that does any meaningful layout computation. – Daniel Wagner May 06 '21 at 20:43
  • You need either the `::[Int]` type annotation or `:set -XMonomorphismRestriction` in order to get `:sprint` to work as expected - see https://stackoverflow.com/a/42602657/12153248 – Ari Fordsham May 07 '21 at 11:29
  • @DanielWagner can you explain why the `Int` elements seem to be forced by `length` rather than just the spine `_:_:_:_`? – Ari Fordsham May 07 '21 at 11:53
  • @AriFordsham I believe the elements are not forced by `length`. There’s simply no thunk to be forced - they are already presented as numbers in `(:)` nodes. – Zhiltsoff Igor May 07 '21 at 12:03
  • 1
    @AriFordsham compare with `xs = map (+1) [1..10]`. Each `(:)` in the thunk contains a link to another thunk: `1 + 1`, or `2 + 1`, or so on. Thus, if you calculate its length and run `:sprint`, it would indeed output `_`s instead of `Int`s. – Zhiltsoff Igor May 07 '21 at 12:07
  • But surely the `Int`s are boxed? – Ari Fordsham May 07 '21 at 12:57
  • @ZhiltsoffIgor of course! – Ari Fordsham May 07 '21 at 13:55
  • @AriFordsham @AriFordsham oh, sorry, my bad. The problem here is that we use `enumFromTo` to declare `xs`, which needs to force the elements to know where to stop. If we declare `xs = [1, 2, 3]`, calculate its length and the run `:sprint`, there would be `_`s. – Zhiltsoff Igor May 07 '21 at 13:57
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/232084/discussion-between-zhiltsoff-igor-and-ari-fordsham). – Zhiltsoff Igor May 07 '21 at 13:59

1 Answers1

2

As @Daniel Wagner explained in the comments, GHCi actually behaves exactly as you expect. It seems to hang because length is extremely fast, and evaluates a huge number of elements, which takes :sprint some time to pretty-print to a string. Unlike ordinary GHCi output, :sprint forces it's string value before it starts printing. If you would wait long enough, :sprint would indeed print the partial string as expected.

You can demonstrate this as follows:

GHCi> xs = [1..100000] :: [Int]
GHCi> :sprint xs
xs = _
GHCi> xs
[1,2,3,4 ... 8504^CInterrupted.
GHCi> :sprint xs
1 : 2 : 3 : 4 : ... : 8504 : _
Ari Fordsham
  • 2,437
  • 7
  • 28