-
Notifications
You must be signed in to change notification settings - Fork 16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add HasCallStack to fail, empty, mzero, asum and msum #301
Comments
Adding |
While both Also, from hackage docs (description of
which means it's a propagating failure in a monadic context. Also, from description of
|
+1 The exception message with
Having even a limited |
Maybe then |
That's not possible. You can't do |
FWIW if it's not used, GHC will optimize it away:
|
Are there any practical reasons why adding |
But someone may argue that the asum :: (Foldable t, Alternative f) => t (f a) -> f a
{-# INLINE asum #-}
asum = foldr (<|>) empty is not correct, and instead it should be defined using sum :: Num a => t a -> a
sum = getSum #. foldMap' Sum i.e. asum :: (Foldable t, Alternative f) => t ( f a) -> f a
asum = getAlt #. foldMap Alt and then, you'd need |
It sets precedent. It's easy to argue that |
I'm in favor of |
I'm non-commitally supportive of adding |
|
Sure, and you could also define a new type that has an "enhanced" behaviour for |
I don't know what that means.
I don't know :) |
Theoretically, something like this could be useful. Does that mean we should add data MaybeT m a = MaybeT CallStack (m a)
mplus ::
MonadPlus m =>
HasCallStack =>
MaybeT m a ->
MaybeT m a ->
MaybeT m a
mplus (MaybeT _ ma1) (MaybeT _ ma2) =
MaybeT callStack (ma1 `Control.Monad.mplus` ma2) |
Would it? How come? Why would you ever want to have a call stack of the last invocation of |
Adding As for "should we add |
It could also be all invocations of Perhaps someone knows a reason it's strictly more important to know the invocation of
I agree, except with the "just going for it" part. There's a bad instance ( |
Big -1 from me. Proliferating
For me, I'm not sure how adding |
I demonstrated clearly why having a The proposal is small, well-defined and its benefits clearly demonstrated. It's a strict improvement over the status quo in terms of easing debugging pains when dealing with a few kinds of errors. If someone wants me to provide more information in the context of the proposal, please do so. I'm not going to engage in any further discussions regarding head-in-the-sky musings in this thread.
Behaviour of any function can branch out based on cleverly placed
This ship has sailed the moment GHC got
In fact, multiple people tried hard to improve over the status quo (https://gitlab.haskell.org/ghc/ghc/-/merge_requests/3236, https://gitlab.haskell.org/ghc/ghc/-/merge_requests/6797) and it eventually got superseded by https://gitlab.haskell.org/ghc/ghc/-/merge_requests/8869 which implements ghc-proposals/ghc-proposals#330 and utilizes
So it doesn't look like an easy problem to fix and |
I am sure you can understand there is a difference between using an unsafe compiler function and using a value advertised in the type signature. Perhaps we could also have You also raise a very good point about it taking over 4 years to navigate the maze of committee bureaucracy in order to attempt to improve the situation. I think after all that, motivation to keep working on improvements grew a little bit thin. However I don't think that it gives the excuse of not doing things in a principled manner, as these small fixes accumulate over time and lead to a messy interface. This seems to me an argument to reduce the amount of oversight and barriers to improvement than any argument in favour of the current proposal. |
You're welcome to engage however you wish. Yet, what you call "head-in-the-sky musings" are an important part of my deliberative process, especially when it comes to the more "mathematical" or "algebraic" designs in I am not, in general, inclined to put purity above pragmatism, nor vice versa. I approach the tension on a case-base-case basis. The case in question, of I'm open to being persuaded otherwise, but that would require engaging with my "headi-in-the-sky musings".
|
I'm not sure if this is fair. Sure you need to compile your program with IPE information to get native backtraces, but that seems less invasive than having to modify Putting aside concerns around principles, what are the concrete costs to this proposal?
In the original post you say these are not issues, but it would be good to see some numbers to back that up. It might be helpful to commit the updated test files on the MR, I'm not sure if we can start a head.hackage build if those jobs are failing. |
I tend to agree with @mpickering about Wrt the process:
I think that's not a fair assessment. The exception proposals were voted on one by one. Part one took 3 months to vote (which I still consider an ok timeframe). The following votes could only happen after that. Part 4 took longer because GHC devs were unsure about the design. I also think that we discussed the interplay between CLC and GHC proposals... the major difference being that GHC proposals don't require an up-front implementation, while CLC proposals do. Afair the consensus was that CLC can give non-binding opinions for GHC proposals that touch base (and don't have an implementation yet). Is there anything else you think that could be improved? |
I'm also in the "algebraic typeclasses" boat. Should we carry around the extra information when working with {- from GHC.Internal.Base -}
-- | Takes the first non-throwing 'IO' action\'s result.
-- 'empty' throws an exception.
--
-- @since base-4.9.0.0
instance Alternative IO where
empty = failIO "mzero"
(<|>) = mplusIO
{- from GHC.Internal.IO -}
-- Using catchException here means that if `m` throws an
-- 'IOError' /as an imprecise exception/, we will not catch
-- it. No one should really be doing that anyway.
mplusIO :: IO a -> IO a -> IO a
mplusIO m n = m `catchException` \ (_ :: IOError) -> n There have always been ways to misuse the instance, and I personally think that rather than infect the rest of my |
This ship sailed a long time ago.
This feels a little disingenuous.
I would be pretty likely to vote in favor of removing these instances. They are footguns. The impact assessment would have to come back pretty strong for me to feel otherwise. However, we could improve people's lives today at virtually no cost. Adding
Honestly, I don't know anything about IPE backtraces, except that they're a "new and upcoming" replacement for We should be focusing on solving real problems. The bad diagnostic information in Please consider the concrete experience of the person who is trying to fix a production system or implement a feature, and they are staring down this message:
Now please imagine telling them: "We could have put a |
My understanding is that IPE backtraces should work in GHC 9.10. I mention 9.14 because that's when the proposed base change (to add HasCallStack) would first appear to users, right? My understanding is that IPE backtraces have been around since GHC 9.4, but the annotated exception proposal was required to make them ergonomic, and that was released in 9.10. So it sounds like there is at least a documentation gap. That's good to know! |
I share the unease over Even the (excellent!) backtraces proprosal doesn't solve this problem, namely, that
That leaves I hope this doesn't sound like I am complaining, since I know it's hard and I really appreciate all of the work that has gone into it. The current state is much better than even just a couple years ago. But, as a user, I still don't know what I should be doing other than using I don't know what the Right ™️ solution is, since -- as many users have pointed out -- there are plenty of functions in |
This is indeed a sad story and I sympathize with the poor soul in this situation. However, it's not the only story we could tell. For example, did the Or did the
Then I would proudly explain "Yes, I managed to get that single-level call stack, by persuading the CLC to essentially retrospectively patch someone else's code." My colleague looks back blankly, wondering how they're going to use that single-level call stack in the debugging process, since it barely gives more clue than the To be clear, I do not think the status quo is good. But nor do I think that slapping the |
I've been through this in practice with |
This is also my experience when all I have are the logs of a web application in production: Even a single line of backtrace remains very useful and saves a lot of time. |
I'm not super deep into error handling like @parsonsmatt ...but I'd like to understand what is missing from the current proposals from @bgamari and co to get us there (without using |
Strongly agree with @Bodigrim and @Kleidukos that even a minimal stacktrace is a huge help.
I think ideally people use hlint to ban bad functions, but they might not do that until they've already been bitten by a bad experience with the function. There are definitely times where you inherit a codebase and it's not up to your standards, and it's also common that you eg ban new uses of a function going forward but grandfather in old uses (to stop the bleeding before you can take on the larger project). |
How about this alternative proposal?
There would be some breakage from 2, but everywhere that a missing instance appears is a place where the maintainer must decide what to do about traceability. I'd argue that's a good thing, because using They might choose the least-effort route and take what's exported from 3. Or they might choose to give a meaningful non-IO exception This would improve the library ecosystem overall. I know it's a tough sell because of the breakage, but in my view the proliferation of |
So far my intuition is
|
With the modern GHC we can deprecate individual instances and I'm generally supporting of adding |
It's difficult to emphasize how foolish I feel telling new hires coming from any other language that if they don't write
I think this is well put. The idea that we need to write Let's add |
I don't think this is the case for |
(I don't find the example of |
A point of information: > let err = throwIO (userError "Important error message")
> err <|> empty
*** Exception: user error (mzero) We can quibble about whether
Indeed not, but firstly there are laws for the interaction of So I don't consider "it's simply monoidal" to be a sufficient refutation of being a "well-defined, principled, mathematical abstraction".
I agree that in practice So the question boils down to: is it better for the community overall to partially remediate the consequences of a badly-written instance, or to preserve the principled nature of a mathematical type class, despite the fact that someone wrote a bad instance for it? Let's do a thought-experiment. Say that we didn't have the instance instance Monoid (IO a) where
mempty = empty
mconcat = (<|>) This version of history is at least plausible. Or alternatively, suppose a popular library defined
I acknowledge this as a fairly strong argument, and on that basis I would be willing to vote in favour, in return for:
Footnotes
|
Exactly. The proposed change is both backwards-compatible and forwards-compatible. If IPE ever becomes a reliable way to obtain stack traces, then HasCallStack constraints in base can be deprecated and dropped. For the record, I don't particularly like HasCallStack either for all of its quirks, but it's the best we have at the moment.
Yet it's not reality.
I care more about UX than ideals. It would be nice if more people in the Haskell community started doing so, maybe the language would become more popular and less rough around the edges.
If I wanted to do this, I would just provide overrides of
The constraint should be removed if (and only if) there's another seamless and reliable way of getting call stacks like IPE. |
Perhaps I didn't explain my point clearly enough: I'm suggesting removing the Anyway, I've explained my point of view and offered a compromise. If you want to stick to your original position then so be it; that is your prerogative as a proposer. My vote will be no. I think it's highly debatable that Haskell will be "less rough around the edges" if we add ad hoc constraints to mathematical type classes. I personally would say that makes it more rough. One could equally say Haskell will become less rough around the edges if people who prefer practicality over ideals don't use mathematical type classes.
Reasoning principles ought to remain valid in plausible alternative realities. Your reasoning principles here seems to suggest that you would add
Right, that would be my suggestion to someone who finds themselves in that position.
But the interface from |
I think there are several things on which we can compromise and still reach a point where the UX of end-users is improved while not tainting our principles.
|
I agree there is a duty of considering possibilities to make people's lives easier, but not at all costs, especially not at making other people's lives harder.
I suggest not bringing morality into this decision.
I completely disagree. This proposed change makes it possible for problems to creep in that are much harder to debug. You might say "well, no one will use import GHC.Stack
import Data.Char
data MyAlt f a = MkMyAlt String (f a)
deriving stock (Functor, Show)
instance Applicative f => Applicative (MyAlt f) where
pure x = MkMyAlt [] (pure x)
MkMyAlt s1 f <*> MkMyAlt s2 x =
MkMyAlt (s1 <> s2) (f <*> x)
instance Alternative f => Alternative (MyAlt f) where
empty = MkMyAlt [] empty
MkMyAlt s1 x1 <|> MkMyAlt s2 x2 =
MkMyAlt (s1 <> s2) (x1 <|> x2)
badEmpty :: (HasCallStack, Alternative f) => MyAlt f a
badEmpty = MkMyAlt s empty
where
s = if even (sum (map ord (prettyCallStack callStack)))
then "Was even"
else "Was odd"
-- ghci> example
-- MkMyAlt "Was odd" []
-- MkMyAlt "Was even" []
example :: IO ()
example = do
print (badEmpty :: MyAlt [] ())
print (badEmpty :: MyAlt [] ()) |
Sorry for the misunderstanding, I said "moral obligation" to distinguish it from a legal obligation, or even being held at gunpoint.
I wouldn't dare being so dismissive, Hyrum's Law teaches us that this would be bound to happen. |
I propose we include
HasCallStack
in the context of the following functions:fail
fromMonadFail
empty
fromAlternative
mzero
fromMonadPlus
asum
andmsum
fromData.Foldable
fail
,empty
andmzero
all signal failure of some sort, but it's currently impossible to retrieve a meaningful call stack when it happens, making debugging harder than it should be.Moreover,
fail
is used implicitly in pattern match failures indo
notation and while the error message includes location of the failing pattern match, it would be much better if there was a possibility of including the call stack.At the moment if one wants to have
Alternative
/MonadPlus
interfaces that report meaningful call stacks for failures, it's necessary to provide specialized versions ofempty
andasum
, kinda defeating the point of the generic interface.asum
andmsum
would also needHasCallStack
since they themselves callempty
/mzero
if all computations on the list fail.Hopefully it's less controversial than #115, because
fail
/empty
/mzero
unconditionally signal failure and are generally small, so during compilation either they are specialized and inlined, or they are not specialized and the overhead of theHasCallStack
is insignificant when compared to the overhead of being in a polymorphicApplicative
/Monad
and calling dictionary functions all the time.asum
andmsum
are small and both already have INLINE pragmas on them.PoC: https://gitlab.haskell.org/ghc/ghc/-/merge_requests/13491 (failing tests are due to expected output changes).
This change is fully backwards compatible.
The text was updated successfully, but these errors were encountered: