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.
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.
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.
The change is being made in bytestring
for functions like head
and tail
: Add HasCallStack by Bodigrim · Pull Request #440 · haskell/bytestring · GitHub
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
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
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.
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).