1

Say I wanted to create a wrapper for UTCTime:

data CustomDateStamp = CustomDateStamp
    { 
      stampValue :: UTCTime
    } deriving (Show, Eq, Ord, Typeable)

Now say I wanted to construct a default for that to "now", e.g.

instance Default CustomDateStamp where
    def = CustomDateStamp getCurrentTime def

This (obviously) fails with:

   • Couldn't match expected type ‘UTCTime’
                  with actual type ‘IO UTCTime’
    • In the first argument of ‘CustomDateStamp’, namely ‘getCurrentTime’
      In the expression: CustomDateStamp getCurrentTime def
      In an equation for ‘def’: def = CustomDateStamp getCurrentTime def
    |
98  |     def = CustomDateStamp getCurrentTime def
    |                   ^^^^^^^^^^^^^^

My question is, how can I use sideeffecty operations inside instance definitions? Is this even possible? What's the preferred approach to this sort of situation?

Abraham P
  • 15,029
  • 13
  • 58
  • 126
  • 2
    This is not a good idea. In a lazy language it is very hard to predict when expressions get evaluated, but on the other hand we do not care that much since purity ensures that the final result is the same. Your instance breaks this property: `let t=def in (t,t)` becomes no longer equivalent to `(def, def)`, since the latter can evaluate to a pair with different components. More pragmatically, `def` might be evaluated only at print-to-screen-time instead of at creation-time, causing very puzzling timestamps. – chi Apr 29 '19 at 16:24

3 Answers3

3

To a first approximation: you can't do that. Once in IO, always in IO (and there is already a Default instance for IO a that doesn't do what you want). Cook up a different plan.

Daniel Wagner
  • 145,880
  • 9
  • 220
  • 380
  • Technically you _can_ do that with usafe-IO, but that won't work the way you expect. – Fyodor Soikin Apr 29 '19 at 15:50
  • 1
    Yes, there is a second approximation which is different from the first. But my answer, and my recommendation, stands. – Daniel Wagner Apr 29 '19 at 15:51
  • Do you think the `Default (IO a)` instance provided "should" be there from a design standpoint? I guess it gives convenience sometimes, at the cost of preventing default-from-(IO-)-environment instances? – moonGoose Apr 29 '19 at 16:31
  • @moonGoose The IO and function instances synergize well: libraries which take a callback, say, `KeyEvent -> IO ()`, can be easily given `def` and that is a sensible default (basically, ignore the event and do nothing). I think I am pretty okay ruling out instances which do actual IO; despite liking `Default` more than most Haskellers, and so probably using it more than most, I haven't really felt any pain from ruling out exciting IO instances. – Daniel Wagner Apr 29 '19 at 17:15
1

I'd throw into the mix that you could write this for a type where the default value requires an IO action,

instance {-# OVERLAPPING #-} Default (IO CustomDateStamp) where
    def = CustomDateStamp <$> getCurrentTime

(easily adjustable for eg. an mtl-stack). Somewhat controversial because overlapping is naughty.

EDIT: Requires OVERLAPPING, IO CustomDateStamp is more specific than IO a so it should select this instance when in scope.

moonGoose
  • 1,510
  • 6
  • 14
  • I agree that would be reasonable in a hypothetical world in which overlapping instances are reasonable. We don't live in that world, however. – dfeuer Apr 29 '19 at 18:15
0

Since getCurrentTime :: IO UTCTime you can't just call it. There is no function with type IO a -> a

Except unsafePerformIO (and other magic stuff like it). I strongly discourage you from taking that route.

talex
  • 17,973
  • 3
  • 29
  • 66
  • 1
    This still won't work the way the OP expects. Depending on reasons, sometimes it will be the actual now, other times it will be some point from the past. – Fyodor Soikin Apr 29 '19 at 15:51
  • This is because there is no meaningful way how it can work, but it will work in first approximation. Because compiler doesn't cache function call result. – talex Apr 29 '19 at 15:54
  • @talex, function call results aren't cached, but this isn't considered a function call in that context. Without optimization it will be implemented using a function call (applying a dictionary argument) and therefore presumably not cached. With optimization, the specializer will almost certainly lift the call out to the top level, and run the `IO` action only the first time a timestamp is requested. – dfeuer Apr 29 '19 at 18:21
  • 2
    The usual advice applies. Beginning Haskell programmers shouldn't use any unsafe `IO` (except what's in `Debug.Trace`). Advanced Haskell programmers know that they should almost never use unsafe `IO`. There are a *very small number* of situations where advanced programmers know they can use it safely (usually in combination with the FFI). In the rest, they will take extreme care and may well ship subtly buggy code until someone trips over it. Yes, that includes the advanced Haskell programmers who wrote the GHC runtime system; this stuff is *hard* to get right. – dfeuer Apr 29 '19 at 18:26