0

In a Haskell program which I don't understand that well (yet), I would like a function

myInfo :: Int -> Picture
myInfo mode =
   ...

to take always 2 seconds longer than normal (to slow down the output).

I looked up Control.Concurrent.threadDelay, but due to its signature threadDelay :: Int -> IO () I cannot figure how to put it in the pure part of the program where the function myInfo is defined.

Is it possible to slow myInfo down (by say 2 secs) without taking the function into the impure area of the Haskell program?

The solution does not need to be production-performant. (It is only a temporary measure to understand the automatic run of the program better.)

halloleo
  • 9,216
  • 13
  • 64
  • 122
  • As far as I know, you can't. Maybe there is a nasty hack like `unsafePerformIO` which migth help, but I don't really know. – lsmor May 08 '23 at 06:59
  • 1
    Pure functions have no concept of the passage of time. Ideally, they all return instantly, or at least as soon as you can compute the answer. Pausing for any period of time is a side effect. – chepner May 08 '23 at 20:15

2 Answers2

6

Sort of – if this is for debugging you can have it as

myInfo mode = unsafePerformIO $ do
  threadDelay 2000000
  return $ actualImplementationOfMyInfo mode

However, while this may be occasionally useful for debugging / testing behaviour with degraded performance, keep in mind that GHC assumes functions are referentially transparent, so you can not rely on this behaviour.

For instance, if you have something like

mode = SomeMode
...

f (myInfo mode)

...

g (myInfo mode)

GHC may well decide that since the two calls to myInfo are identical to only evaluate it once and reuse the result later. If you need predictable time based / sequential behaviour you're not going to get around having to get your logic into IO, one way or another.

Cubic
  • 14,902
  • 5
  • 47
  • 92
  • Thanks @Cubic, the hack is for debugging/understanding the program, so I will try your suggestion. – halloleo May 08 '23 at 23:56
  • Also your delay may be run once or twice or more times or never at all, and possibly not at the points in execution where you expect it to. That's just what you're signing onto when you invoke `unsafePerformIO`. There are valid uses of upio, and ways to rein it in (using `seq`, bang-patterns, etc.), but it's something to steer clear of unless you _really_ know what you're doing. – A. R. May 09 '23 at 20:56
0

The fundamental characteristic of a Pure Function in Haskell is that it does not have any side-effects. Whenever that function is called with the same inputs, it will always produce the same output.

Any timing parameters - such as how long it will take for a function to run - are side-effects, as they depend on external factors, such as for instance your system load, available memory and the phase of the moon.

None of these are inputs to your function - and hence, it's outputs may not depend on them.

Furthermore, computations in Haskell are lazy by default - that is, your myInfo won't even get evaluated until something consumes it's output.

That being said - and before diving in some of the ugly hacks - instead of attempting to add any side-effects to a pure function, the primary focus here should be on what exactly are you trying to accomplish and why is that needed.


Most likely, what you want to do can be achieved by putting the "impure" slowdown into whichever code is calling that pure function.

If it's called from other pure code, keep going "up" until you get to the "IO" monad and do it there.

Due to the "lazy" nature of pure functions, the effects that you want to slow down won't really manifest until you try to pass it's output to "IO".


If you absolutely cannot avoid putting the slow-down into that function, then instead of using that function that allegedly kills a Kitten each time it's called - you might want to look into using STM with a "pulse".

The STM library and it's "Batteries included" counterpart provides synchronization primitives that allow your function retain it's "pureness".

You could for instance use TMVar to use something similar of a POSIX Semaphore - and flag it from a "pulse" thread.


I am still fairly new to Haskell myself, only started to use it about two and a half years ago, but I had quite a few of those "impure code" issues - and when I spent some time thinking about them and trying write down what it was that I was actually trying to accomplish, I realized there was a much better way of doing it.


/Edit: To address the "add super expensive computation" fallacy.

One might think about "just" adding some "expensive computation" to that function.

But that fails to address two fundamental issues:

First of all, why this is even attempted in the first place. So far, the OP has only asked a curious question about a topic, and he certainly deserves to get the best possible answer to that.

But that doesn't automatically imply that there might not be a better solution to his problem than what he initially came up with. Instead of locking ourselves up in this mindset that we absolutely need to make this function impure - we really ought to explore different options.

Instead of going down this rabbit hole of adding hack after hack to work-around things, we need to go deeper and look at what the fundamental issue is.

There is no guarantee that GHC won't progressively get better at detecting and eliminating such seemingly useless fluff.


To help create a great solution, I have marked this as Community wiki, so please feel free to edit and improve this as much as you want.

Martin Baulig
  • 3,010
  • 1
  • 17
  • 22
  • Timing is not a side effect that is observable from pure code. So adding a delay to a function does not make it impure. In fact, you can do it by just running a useless expensive computation (only then you don't have precise control). See [this discourse thread for a discussion of what purity really is](https://discourse.haskell.org/t/using-unsafeperformio-safely/4146/78?u=jaror). – Noughtmare May 08 '23 at 11:03
  • "Timing is not a side effect that is observable from pure code." - that's pretty much what I said. But then, it's logical consequence is that you also cannot rely upon it in pure code. – Martin Baulig May 08 '23 at 11:15
  • You have no control over when your "useless expensive computation" will be run - or whether it'll even be run at all. Since it's useless fluff, even if it worked today, a future version of GHC could get better at eliminating it. – Martin Baulig May 08 '23 at 11:18
  • 1
    I think what I'm trying to say is that I don't agree that timing is a side effect, otherwise every function (that takes time) is impure. Instead I think it is more accurate to say that time does not exist in Haskell. – Noughtmare May 08 '23 at 11:39
  • Is that "batteries included" link right? It doesn't look related to STM to me. I also don't understand how STM would let you avoid the kitten-killer. – Daniel Wagner May 12 '23 at 22:10