-
Notifications
You must be signed in to change notification settings - Fork 631
[CSL-1842] Write the guidelines for exception handling #2059
Conversation
07a9f08
to
b06c460
Compare
docs/exceptions.md
Outdated
Do *not*: | ||
|
||
* use `error` or `impureThrow` | ||
* return `Either Text` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd add MonadFail
to this list.
docs/exceptions.md
Outdated
|
||
Use `bracket` or `ResourceT` to guarantee the release of resources. In case of | ||
concurrent code, avoid `forkIO` or `forkProcess` in favor of the `async` | ||
package, as it rethrows exceptions from the child threads. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd write that ResourceT
should be avoided if possible and only used when bracket
is not enough. It was recommended during one of HBP meetings and I totally agree, because we had several bugs with improper usage of ResourceT
.
docs/exceptions.md
Outdated
|
||
We should get rid of `Mockable Throw` and `Mockable Catch`, as they buy us | ||
nothing compared to `MonadThrow` and `MonadCatch` but have less ecosystem | ||
support -- for instance, the `safe-exceptions` package doesn't use them. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd also get rid of Mockable Bracket
. Unlike Mockable Throw
and Mockable Catch
, there is no equivalent class in exceptions
(MonadMask
is not equivalent, it's strictly more powerful), so there is the reason to have Mockable Bracket
, but I think that the advantage of using MonadMask
instead (e. g. that foreign libraries like safe-exceptions
are aware of MonadMask
) is more important.
Do *not*: | ||
|
||
* use `error` or `impureThrow` | ||
* use `ExceptT`, `MaybeT`, or `CatchT` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I personally think that using ExceptT
or MaybeT
in impure code is sometimes reasonable, as long as it's not exposed by the interface.
Here is a real example where I find it quite useful and convenient:
getSyncStatus lagBehindParam = |
getSyncStatus
has type SlotCount -> m SyncStatus
. It doesn't mention IO
directly, so it's potentially impure code.Its implementation is
IO
-based (it has MonadIO
constraint, also MonadThrow
constraint via MonadDBRead
). There we want to check several conditions and exit immediately when some of them are met.ExceptT
works really nicely in this case.Note that the interface doesn't expose
ExceptT
or MonadError
.There are two alternatives:
- Use
throwM
and thencatch
(or another catching function liketry
) outside maindo
-block. I don't like it because throwing an exception and catching it within the same function looks weird and because makingSyncStatus
anException
is a bit insane (I don't think we can call it an exception in common sense). - Use a lot of nested
if
s orcase
s or similar language constructs. I don't like it because then code will look really ugly.
Maybe you didn't intend to prohibit such usages, but it should be explicitly clarified then.
You can also add this example to Code references
section below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am still not convinced that using ExceptT
here is a good idea. I believe that it can be safe when used carefully, but:
-
now you don't have
MonadMask
instance, meaning that you might need to rewrite code if it needs to allocate resources in the future -
now you have to lift through an additional transformer, which might contradict the practices we're establishing in CSL-1823
-
the rules for exception usage become more nuanced: is it ok to do this if it's in a helper function? what if I move this helper function to top-level? what if I put it into a separate module for reuse?
I've seen your comment that you want to allow usage like this, but I don't think that it pulls its weight. Are you sure that doing throwM
and try
wouldn't give you the same thing?
The transformation is mechanical:
fmap convertRes . runExceptT $ do
-- becomes
fmap convertRes . try @SyncStatus $ do
True -> throwError SSDoingRecovery
-- becomes
True -> throwM SSDoingRecovery
Adding an Exception
instance does not sound bad to me, it's derived by GHC so you don't have to write any code.
It looks like we get the same benefits without any disadvantages if we throwM
here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
now you don't have
MonadMask
instance, meaning that you might need to rewrite code if it needs to allocate resources in the future
I still have MonadMask
instance, I can just use lift
where I need it. The only place where I can't use lift
is throwError
, but I don't need MonadMask
there.
now you have to lift through an additional transformer, which might contradict the practices we're establishing in CSL-1823
It's hard to talk about it until we come to a conclusion about CSL-1823, but I am already a bit concerned about that. If we are going to prohibit all lifting, what will we do with our tests which live in PropertyM
? There we use lift
in many places. AFAIU, ExceptT
case is the same. Maybe it should be discussed in CSL-1823, not here. Maybe I am just missing something.
the rules for exception usage become more nuanced: is it ok to do this if it's in a helper function? what if I move this helper function to top-level? what if I put it into a separate module for reuse?
It's not hard to come up with concrete rules. For example, we can prohibit ExceptT
in types of all top-level impure or potentially impure functions. If you have a nested function which has ExceptT
in its type and you want to make it top-level, you can change its type to m (Either Patak Sepulka)
and convert it to ExceptT
where it's used (which is straightforward).
It looks like we get the same benefits without any disadvantages if we
throwM
here.
Yes, I agree that there no obvious disadvantages in using throwM
. I never had good arguments why ExceptT
is preferable in this case. It's mostly based on my personal feeling/intuition, rather than some fact that make ExceptT
definite winner.
- For me
SyncStatus
conceptually is not an exception, that's why I find it a bit counterintuitive to throw it as runtime exception. And defining itsException
instance. It's not about writing extra code, it's just about intuition behind this.ExceptT
name also suggests something exceptional, but I treat it simply as a transformer which provides multiple exit points. ExceptT
version is basically just sugar for the version with multipleif
s/case
s. And the version with multiplecase
s is exactly how we want the code to behave. It's really clear how it works if you desugar it. On the other hand, version withthrowM
involves real runtime exceptions which are implemented somehow.
Anyway, as I said, I don't have and never had solid arguments in favour of ExceptT
in this case, it's mostly my personal preference. I can easily discard this preference if others prefer throwM
version in this case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Btw, with throwM
approach someone can make joke and throw SSInFuture
from e. g. DB.getTipHeader
function and we'll catch it and return SSInFuture
, though perhaps we shouldn't catch it.
It's something exotic and nobody will do it of course, so you can ignore it, just something came into my mind :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still have
MonadMask
instance, I can just use lift where I need it.
Not quite, you can't do something like this:
runExceptT $ do
doStuff
bracket ... $ do
if b
then throwError e -- won't work
else return x
If we are going to prohibit all lifting, what will we do with our tests which live in PropertyM?
I believe we can use it as our base monad, because the only transformer on top that we want is ReaderT
and since adding ReaderT
is equivalent to adding a parameter to a function, I believe it should be always possible.
If you have a nested function which has ExceptT in its type and you want to make it top-level, you can change its type to m (Either Patak Sepulka)
But returning m (Either Patak Sepulka)
isn't recommended either, you should return m Sepulka
and throw Patak
. The reason is that you wouldn't want to deal with the possibility that Patak
might be both thrown and returned in Either
. Perhaps we can invent a rule that it's OK to return something as Left
if and only if it doesn't have an Exception
instance (preferably, it should have an instance like instance TypeError "NOT AN EXC" => Exception Patak
to avoid accidents).
It's mostly based on my personal feeling/intuition
I share your feelings, I see value in using ExceptT
in this case too. But I'm having a hard time to come up with good, simple and precise rules, to detect these cases, so let's settle for a more uniform (but perhaps not as nice) solution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It sounds quite sad. So AFAIU in some cases we have to use exceptions for control flow, which is generally considered anti-pattern, at least in some languages. Maybe it's not a big problem for Haskell, though. Or we can write deeply nested code, but it's also sad.
I am not saying that we shouldn't do it, I just don't like this world :/
Regarding your points:
Not quite, you can't do something like this:
Well, there is a workaround to return Either
from inside bracket
and then have ExceptT $ bracket …
. If it's too unpleasant, you can use exceptions instead. Permitting ExceptT
in such cases doesn't mean that one must always use ExceptT
. It's just another possible approach (which will sometimes be useful).
I believe we can use it as our base monad, because the only transformer on top that we want is
ReaderT
and since addingReaderT
is equivalent to adding a parameter to a function, I believe it should be always possible.
As I wrote somewhere, having PropertyM
in base monad will leave us without MonadMask
instance, which is probably too bad. Anyway, perhaps we shouldn't continue discussing it here.
But returning m (Either Patak Sepulka) isn't recommended either, you should return m Sepulka and throw Patak…
- I think we can prohibit returning
Either
from potentially impure functions. We can always define an isomorphic ADT. E. g. we haveverifyBlocksPrefix :: OldestFirst NE Block -> m (Either VerifyBlocksException (OldestFirst NE Undo, PollModifier))
and we can change its return type tom VerifyBlocksRes
wheredata VerifyBlocksRes = VerifyBlocksSuccess (OldestFirst NE Undo, PollModifier) | VerifyBlocksInvalid VerifyBlocksException
. But maybe it would be too strict restriction.Either
doesn't always carry something exceptional as itsLeft
. For instance, we havetype Block = Either GenesisBlock MainBlock
. This rule (the version without «it's OK to return something as Left if and only if it doesn't have» part) would mean we can't returnm Block
. Yes, we can and should change it do another ADT instead of usingEither
. But do we want to create an ADT for everything isomorphic toEither
?
Also, let's considerVerifyBlocksRes
from above.VerifyBlocksException
might have anException
instance. Should we care about possibility tothrowM
it? It's notEither
, but isomorphic. What ifVerifyBlocksRes
has 10 constructors only one of which has anException
instance?
Alternatively we can changeverifyBlocksPrefix
's type toOldestFirst NE Block -> m (OldestFirst NE Undo, PollModifier)
. My intuition suggests that we shouldn't do it, but maybe it's another case where I should ignore intuition. - I like this part:
Perhaps we can invent a rule that it's OK to return something as Left if and only if it doesn't have an Exception instance (preferably, it should have an instance like instance TypeError "NOT AN EXC" => Exception Patak to avoid accidents).
- Exceptions Best Practices suggests that returning
Either
is ok. Maybe not ideal but not something to prohibit. At least that's how I treat this phrase:
Generally the solution to the ExceptT IO anti-pattern is to return an Either from more functions and throw an exception for uncommon errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does anyone else have an opinion on this? Should we add a rule about allowing local use of ExceptT
in impure code and returning Either
, as long as the type has no Exception
instance? I do not have a strong opinion, but I'm hesitant to add more complications to the rules.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the rule to use ExceptT
only with TypeError
exception instances for types. I'm not aware of better solution for early exit from function (I'm aware only of Cont
-monad solution but I can't call this solution a better one ;) ).
docs/exceptions.md
Outdated
|
||
We should make sure that no code imports `Control.Exception` or | ||
`Control.Monad.Catch`, and use `Control.Exception.Safe` instead. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wrote few more things which I want to be done. But I admit they might take quite a lot of time and I don't know how much time we can spend on this Epic.
If I had to choose only the most important points from those, I would choose these three 👍
But I think it'd be good to also add other improvements which I suggested in comments to CSL-1842 (avoiding Text
for errors, avoiding exceptions leakages due to fork
s, etc.). Maybe we don't have time to do them within this Epic, but at least it will provide broader picture of the problems we currently have.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
avoiding Text for errors, avoiding exceptions leakages due to forks, etc
I included these in the guidelines, but not in the migration plan. I'll make it more explicit and copy these to the migration plan.
|
||
* use `error` or `impureThrow` | ||
* use `MonadFail` | ||
* return `Either Text` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not? Sometimes we have someFoo = runExceptT someFooDo
because every usecase would anyway run runExceptT
on the someFooDo
. Or do you mean that returning Text
error is bad? It's a reasonable tradeoff, i guess.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is about Text
. Define proper ADTs for your errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't see why is it always reasonable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I answered in the neighboring thread where you made the same comment.
docs/exceptions.md
Outdated
* `MaybeT`, `ExceptT e` | ||
* `CatchT` | ||
|
||
Avoid using `Text` with the error message in place of `e` -- create a proper |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, I disagree. Consider that we're writing a message handler. What impureFunc
returns is either data or error message to user. Why would one use adt here? We never catch
this error.
listener msg =
either respondError respondSuccess =<< impureFunc msg
Rephrasing, sometimes Either A B
is not a "way of exception handling", but is a handy way to return a result. One more example: let checkData
be a impure function that has possible outcomes:
- Exit with success ~ "data is alright"
- Exit with failure ~ "data is not alright because T"
- Exit with normal error ~ "database became inaccessible (if we communicate w/ it over web)" or "we tried to write a log to file, but it doesn't exist".
- Exit with programmer error ~ "something went really wrong and it's not expected"
What you seem to miss is that sometimes it's easier to represent 1/2 difference with Either Text a
instead of a custom ADT like DataVerSuccess | DataVerErrorX | DataVerErrorY | ...
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Defining an ADT in Haskell is cheap. You never know what the caller of your function might want to do with the Left
case, perhaps it has a way to handle specific cases. You assume that the only thing done to the error is displaying it, which is a strong assumption, and may change with time.
To be clear, this rule applies to top-level functions, where you cannot know what the caller intends to do with the return value. Maybe we want to log the error as JSON rather than text, maybe we want to return it from a HTTP request (and assign an appropriate error code), maybe we want to display it in a different language (Japanese), etc...
Just be nice to the consumers of your API and don't return Text
. Instead, return an ADT
and a function (or a Buildable
instance) to display the error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@int-index Ok, I support your point in fact, but I think it should be explicitly stated in the document. It's not that straightforward and clear why do we ban using Either
for returning non-exceptional failures.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to keep the guidelines to the point, without justifications, so people can use them as a quick reference to make a decision. Good thing that this GH discussion will remain and can be linked to in case there are questions.
DISCUSSION: Should we create a synonym `bug = impureThrow` in Universum? This | ||
would make the intention more clear. | ||
|
||
### Pure code, regular errors |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regular error means
In cases when the erroneous scenarios are out of our control, we consider these to be regular errors.
but in a pure program, nothing is out of our control, so does "Pure code, regular errors" make any sense?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, technically yes, we do control what our function take as input. But consider these three ways to give a type signature to head
:
[a] -> a
NonEmpty a -> a
[a] -> Maybe a
In case (1) we assume that the caller has full control of the passed argument, and passing []
is a programmer mistake. If the provenance of the input list is external (e.g. we've read the list from a file), we (as a caller) need to check whether the list is empty. In case (2) we ensure this invariant on types. But in case (3) we move the check into the function itself, rather than impose it on the caller. You could say that this is not an "error path", but informally you would normally think about this as a failure (we tried to extract the head
, but there isn't one).
If we tried to formalize this, I would say in a pure function we can talk about an expected set of codepaths, and we also have checks that ensure that the input satisfies the necessary conditions for this codepath. When the checks fail, we want early exit from the function — this is what I mean by "regular errors in pure code".
I agree that it's not a strict definition, it depends on what you consider to be an error, but that's how we usually reason about code informally.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The lesson here is that head :: [a] -> a
is a lie :)
I revise my statement then: in a total pure program, error throwing / handling doesn't make sense. There can be programmer mistakes, sure, but the function is total so in these cases the programmer didn't realize there was a mistake.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even in total functions, can't we talk about having a "major" codepath and various "error" codepaths? For instance, when we consider parsing, "parse errors" are called "errors" for a reason, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right it's confusing terminology. It's a parse error, but a parse error is not an error in parsing. Parsing went just fine: we successfully determined that the input string is not in the language. A better terminology would perhaps be parse :: Parser a -> Either Unrecognized a
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, I would consider it an error in user input, as opposed to an error in our program. This is the distinction: when input data (or outside world in general) does not satisfy the properties we want (but we have to handle it), I called this a regular error. When our program has a bug, I called this a programmer mistake.
Can you propose better terminology? I do not insist on calling both of these "errors", but there's already a (confusing) tradition to call both of these errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tend to use "error" to mean a programmer mistake i.e. a bug. Something that could be avoided but was not, because we're human. So regarding head :: [a] -> a
, yes, the error would be the caller's error, because he was just supposed to know that the input must not be []
.
I tend to use "exception" to mean something that the programmer could not possibly have defended against / ruled out and is only capable of handling / responding to. There's no exception in head []
, it's a plain error. But reading from a TCP socket that has been reset is an exception. The programmer did nothing wrong, the connection just broke. Failing to handle an exception could be considered an error.
I guess you could also speak of "user error" but that's very different. It doesn't mean any errors in our program. A parse error could be a user error (user gave a malformed config file or something). It can be considered an error to fail to deal with a user error.
`IO` actions inspected/used in the code? | ||
|
||
* Yes: it is potentially impure code | ||
* No: it is 100% pure code |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not so sure about this one. f :: forall m . Monad m => m ()
is pure, even though it can be specialized to IO ()
. The specialization can never throw an exception / do other side-effects, because forall m . Monad m => m ()
doesn't permit this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, it is classified as "potentially impure", because it can be instantiated to IO
. This means we don't want to use ExceptT
there, because it could do IO
.
For instance, assume that we have f :: forall m. Monad m => m a -> m a
. Suppose we're passing an IO a
, and it throws. Inside f
we could have used ExceptT
that assumed that it was the only way to throw, but now it isn't.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems fine to use ExceptT
in the definition of f
. The resulting forall m . Monad m => m a
cannot introduce any of its own throws, so the use of ExceptT
is not really adding a new avenue of exception throwing to that term. If f act :: IO a
throws an exception we are 100% sure it was thrown by act
, and if act
has only way exception throwing mechanism then so does f act
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, perhaps this definition is overly restrictive. Could we come up with a more precise one?
Consider parsing: it is pure, but we cannot make assumptions about the input. In | ||
this case we might want to use `ExceptT ParseError`. Or consider a lookup in a | ||
`Map`, where we don't know whether the key is present -- in this case we'd like | ||
to return `Maybe v`. In 100% pure code, use one of these ways to handle errors: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A Left ParseError
from parsing or a Nothing
from a map lookup is not an error / exception, it's completely normal. If I get a parse error, nothing has gone wrong, the parser simply decided that the input was not in the language. When I get Nothing
from a map lookup, nothing has gone wrong, the thing just wasn't in the map... completely normal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's why this is classified as "regular errors", everything is normal, we just have to handle these cases, which are not the main (desired) flow of events.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right my issue is with the term "error". They're not errors, they are part of the main desired flow of events.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What you consider an error depends on your expectations. When you readFile
and the file does not exist, is it an error? No, we should've expected this. When you allocate a chunk of memory and the heap is full, is this an error? No, we are aware that the events can take this turn. If you think this way, nothing is an error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When you readFile and the file does not exist, is it an error? ... When you allocate a chunk of memory and the heap is full, is this an error?
Yes and yes. Strictly speaking I'd call them exceptions but let's not get hung up on semantics of error vs. exception.
What you consider an error depends on your expectations
You're absolutely right. When I parse something I expect that the input string may not be recognized. When I lookup a key in a map I expect that it may not be in the map. Nothing went wrong in these cases. But with readFile
, and with IO
in general, things may go wrong for reasons outside of the programmer's control (reasons external to the program itself), and there's no obvious one-size-fits-all response to it, so we resort to exception handling.
docs/exceptions.md
Outdated
|
||
* return `Either ErrorADT`, `Maybe` | ||
* wrap the underlying (pure!) monad in `ExceptT` or `CatchT` | ||
* use `MonadError` or `MonadThrow` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If one uses MonadError
or MonadThrow
, code stops being pure (because m
can be e. g. ExceptT () IO
or just IO
for MonadThrow
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can write code in a concrete pure monad but use methods from MonadError
and MonadThrow
, that's what is meant here. For instance, when you write code for CatchT
, you use methods from MonadCatch
.
docs/exceptions.md
Outdated
|
||
Use `bracket` or to guarantee the release of resources. In case of | ||
concurrent code, avoid `forkIO` or `forkProcess` in favor of the `async` | ||
package, as it rethrows exceptions from the child threads. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes and prefer withAsync
/ concurrently
/ race
wherever possible over async
itself, because that one will not automatically kill the child if the parent dies. withAsync
basically does a bracket
so don't use it in a tail-recursive loop like loop = waitForData >>= \it -> withAsync (process it) >> loop
, and be aware of link
in case you don't plan to wait
for the Async
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that's the way to use async
that I had in mind. I added a clarification.
Do: | ||
|
||
* try to use types to avoid the need in the first place | ||
* comment extensively (invariants and precondition, reasoning) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, it might be even better to use LiquidHaskell
for this. I think this is exactly the use case for LiquidHaskell
— verify some invariants on compilation state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, we could experiment with LiquidHaskell
in the future. Right now it does not seem mature enough for a project on the scale of Cardano, but I'd like to be proven wrong.
it's better to not overcomplicate code. Use your judgement.) | ||
|
||
DISCUSSION: Should we create a synonym `bug = impureThrow` in Universum? This | ||
would make the intention more clear. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't mind adding bug = impureThrow
to universum
. With HasCallStack =>
of course.
You're welcome to open issue here: https:/serokell/universum/issues
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, created serokell/universum#100
`MonadThrow`). | ||
|
||
Derive prisms for exception types with multiple constructors, so it's convenient | ||
to use them with `catchJust`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any particular reason to use prisms exactly instead of affine traversals? I understand that word prism is much more clear for people. But usage of Prism
type class enforces to use lens
library instead of some cheaper alternative, like microlens-platform
. As for me. even for such big project like cardano-sl
package microlens-platform
should be enough. Though, I guess there're a lot of dependencies which use lens
package by themselves so we can't optimize dependencies here much...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we want Prism
to have both construction and matching.
Do *not*: | ||
|
||
* use `error` or `impureThrow` | ||
* use `ExceptT`, `MaybeT`, or `CatchT` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the rule to use ExceptT
only with TypeError
exception instances for types. I'm not aware of better solution for early exit from function (I'm aware only of Cont
-monad solution but I can't call this solution a better one ;) ).
docs/exceptions.md
Outdated
itself when you can use `withAsync`, `race`, or `concurrently`). | ||
|
||
When resource usage is non-linear, it's okay to use `ResourceT`, but prefer | ||
`bracket` whenever possible. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please clarify what is non-linear resource? It's not a common terminology... I'm aware of Linear Types and I can imagine which resources can be called linear but still want to make sure that terminology is the same for everybody.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@int-index please clarify.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Non-linear is anything that doesn't fit into the "allocate, use, deallocate" pattern.
docs/exceptions.md
Outdated
|
||
* use `error` or `impureThrow` | ||
* use `ExceptT`, `MaybeT`, or `CatchT` | ||
* use `Maybe` or `Either` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- What's wrong with
Maybe
? - AFAIU, you mean that
Either
shouldn't be used to represent regular errors, but it's fine to returnEither SomethingThatIsNotAnError SomethingElseThatIsNotAnError
, is it correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Example: is it ok to return Block
? Currently Block
is Either GenesisBlock MainBlock
. Of course it can be changed to a new type which is not Either
, but suppose we don't want to do it for some reason. For example, because it's not trivial and will take some time, but one may want to return Block
from a function right now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea was to have uniform throwing principle for potentially impure code (i.e. always throw an exception with throwM
). Now that we plan to relax this rule to allow Either NonExc NonExc
, then allowing Maybe
makes sense as well.
So, if my understanding is correct, there are the following concerns which should be addressed before adopting the guidelines from this PR:
|
Here is what I propose to do:
|
@gromakovsky I think I'm not who you want to see here =) |
@gromakovsky I agree with your plan, will you incorporate the changes into the document? I answered the questions about non-linear resources and |
Ok, will do it after I get quite confident that everyone is fine with that. |
Yes, I reviewed them back when you pushed. |
TODO: write that |
@avieth do I understand correctly that your concerns in https:/input-output-hk/cardano-sl/pull/2059/files#r156155221 and https:/input-output-hk/cardano-sl/pull/2059/files#r156127346 are only about terminology? Do you agree that using If it's only about terminology, can you propose better terminology? |
769e2e6
to
1133dc7
Compare
1133dc7
to
188d594
Compare
188d594
to
f797ad9
Compare
Rendered: https:/input-output-hk/cardano-sl/blob/int-index/csl-1842/docs/exceptions.md