Is adding HasCallStack a breaking change?

Does adding HasCallStack constraints to an API constitute a breaking change (and consequently a major version bump)? Strictly speaking, it is a change of types, and change of types is considered breaking. But on the other hand, this change cannot break any client code! The only possible failure might happen if clients catch error in IO and analyse its message. So it is equivalent to changing error messages, which I would not generally mark as a breakage.

7 Likes

I lean towards it not being a breaking change. I would mention it in the change log, but I would feel comfortable releasing it with a minor or patch release.

5 Likes

I agree with Taylor: I think that a minor version bump would be appropriate in most packages.

I would bump the major version in packages where analysis of error messages is common/likely in the usage of the software. In addition to analysis of error messages within Haskell, one may need to be very careful about changing error messages if the error messages are used outside of Haskell. Here are some examples that I have seen before:

  • analysis of the error message within a (cron) script
  • configuration of alerts that are triggered by scanning logs (common in AWS)
  • storage of logs/errors in a database (Prometheus, etc.)

The decision may be difficult when the change is made in a general library (on Hackage). In this case one does not know how the users may be using the error messages, but applications where error messages are significant should have unit tests to alert developers of any significant changes.

3 Likes

The change is being made in bytestring for functions like head and tail: Add HasCallStack by Bodigrim · Pull Request #440 · haskell/bytestring · GitHub

3 Likes

Personally, I think that a minor version bump would be fine in this case. These errors are caused by unsafe usage of partial functions, and I generally do not see alerts or business logic created for such development issues.

I thought about it and I do not consider this a breaking change.

The important question is whether code that uses this function continues to compile after adding the constraint. That seems to be case: The following code compiles regardless of whether foo has the constraint or not.

foo :: HasCallStack => String
foo = "foo" ++ undefined

bar :: String
bar = "bar" ++ bar

In contrast, ordinary typeclass constraints typically require propagate to the site of usage.

After torturing GHC a little bit, I’ve managed to come up with an example that produces different runtime results after adding HasCallStack to the top-level function (on GHC 8.10.7). Thus being said, the example is completely artificial and probably doesn’t occur in real life. And, AFAIU, PVP tells only about compile-time guarantees, not runtime. However, in my practice, I’m trying to follow the rule “when in doubt, bump up major version”. I feel that people have better feelings about manual upgrades than about breaking changes during minor version bump ups.


Anyway, here is an example. Consider the definition of the callerName function from relude:

callerName :: HasCallStack => String
callerName = case getCallStack callStack of
    _:_:caller:_ -> fst caller
    _            -> "<unknown>"

If we have the following code:

{-# LANGUAGE RankNTypes #-}

module Stack where

import GHC.Stack.Types (HasCallStack)
import Relude.Extra.CallStack (callerName)


when :: (Applicative f) => Bool -> (HasCallStack => f ()) -> f ()
when False _ = pure ()
when True  g = g

myApplication :: HasCallStack => IO ()
myApplication = do
    putStrLn "Here"
    when True $ putStrLn callerName

It prints:

Here
<unknown>

However, if we now add HasCallStack to when:

when :: (HasCallStack, Applicative f) => Bool -> (HasCallStack => f ()) -> f ()

We get:

Here
when
2 Likes

Ah, a very interesting example, thanks @ChShersh. In my case partial functions do not take callbacks and do not analyse call stacks, so it’s probably fine.

Yes, normally I’d bump a major version as well. Except that everyone will hate me if I bump a major version of bytestring :slight_smile: Not gonna happen any time soon. So the choice is either leave users with head: empty bytestring errors without call stacks for two or three years more, or decide that this is a minor change.

The problem with boot libraries is that users do not get a free rein, an upgrade is forced upon them by GHC anyways.

I realised that actually any change affects error messages: even without HasCallStack they contain an exact line and column of error in source code. So change of messages should not be considered a breakage.

3 Likes

:exploding_head: Wow, this is the first time I am learning that callStack does not require running in IO. How on earth was this approved? The internal hacks to automatically satisfy HasCallStack constraints work will in practice but they utterly break expectations so anything that uses them should run in IO!

Anyway, this a complete tangent,but I was just gobsmacked. Anyone who wants to discuss that further might like to do so here:

It’s a non-breaking change since those functions should never be called (only half-joking).

2 Likes