Skip to content

Conversation

Jimbo4350
Copy link
Collaborator

@Jimbo4350 Jimbo4350 commented Jan 28, 2025

Re-added ADR 8 to easily access discussions regarding the ADR

@Jimbo4350 Jimbo4350 force-pushed the jordan/adr-8-RIO-in-cardano-cli branch from c389d3e to c29a9d8 Compare January 28, 2025 15:38
@Jimbo4350 Jimbo4350 requested review from a team as code owners January 28, 2025 15:38
Copy link
Collaborator

@carbolymer carbolymer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs more details how do we go about it

@Jimbo4350 Jimbo4350 force-pushed the jordan/adr-8-RIO-in-cardano-cli branch 4 times, most recently from 3126aee to af8684b Compare January 29, 2025 20:35
Copy link
Collaborator

@carbolymer carbolymer Jan 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ADR does not explain how this type should be used. You can't fromException other exceptions into CustomCliExceptions, so it will be useful only when explicitly thrown.

Is this a temporary solution to bridge between ExceptT errors and exceptions, or a final design?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't fromException other exceptions into CustomCliExceptions

I don't understand this objection. Can you clarify?

Is this a temporary solution to bridge between ExceptT errors and exceptions, or a final design?

This is a suggestion of a final design.

The purpose of CustomCliException is to represent explicitly thrown, structured errors that are meaningful to our application. For other exceptions (e.g., runtime errors or unexpected failures), we rely on the Nothing case in our handler.

Copy link
Collaborator

@carbolymer carbolymer Jan 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this objection. Can you clarify?

You've answered my question in the next paragraph of your comment. 👍🏻

This is a suggestion of a final design.

The purpose of CustomCliException is to represent explicitly thrown, structured errors that are meaningful to our application. For other exceptions (e.g., runtime errors or unexpected failures), we rely on the Nothing case in our handler.

Ok. The thing is, that explanation should be in the ADR itself, so that it's clear for the next person reading it.

Copy link
Collaborator

@carbolymer carbolymer Feb 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a suggestion of a final design.

This does not sound like a suggestion to me, more like a strict rule:

All errors are caught as CustomCliException.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will be a strict rule when the ADR gets merged. I.e I want the team's buy in.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All errors are caught as CustomCliException.

This is nowhere demonstrated.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. ADR 7 says we should log to stderr
  2. I'm not quite seeing the point of differentiation between two cases: CustomCliException and the other one. You're doing displayException in either case.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. We can log to stderr.

You're doing displayException in either case.

Look at the instance Exception CustomCliException. It requires an Error e instance for pretty rendering and includes a call stack for debugging.

I'm not quite seeing the point of differentiation between two cases

This ensures we capture all exceptions. We handle CustomCliException explicitly for structured, user-facing errors, while the Nothing case acts as a safety net for unexpected exceptions.

Copy link
Collaborator

@carbolymer carbolymer Feb 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ensures we capture all exceptions.

Catching SomeException would do the same here.

We handle CustomCliException explicitly for structured, user-facing errors, while the Nothing case acts as a safety net for unexpected exceptions.

Again, this separation is not shown anywhere in the code and is not explained in the text. A next person reading this ADR will have no clue what is the role of CustomCliException.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Catching SomeException would do the same here.

Are you saying to explicitly pattern match on this? I don't understand your point here.

Copy link
Collaborator

@carbolymer carbolymer Feb 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you saying to explicitly pattern match on this? I don't understand your point here.

Yes, SomeException is a catch-all for all exceptions (asynchronous as well).

Copy link
Collaborator Author

@Jimbo4350 Jimbo4350 Feb 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this different that the Nothing case? It's going to handle all other exceptions via displayException.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly, it is not - so what's specific about CustomCliException to pattern match it in the handler?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Loss of Specificity: Existential quantification erases concrete error types, preventing pattern-matching on specific errors.
- Loss of Specificity: Existential quantification erases concrete error types, preventing pattern-matching on specific errors. That may make specific error recovery logic harder to implement.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Converted how? This is not explained in the examples.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linking to a whole PR from an ADR is not ideal:

  1. That PR is too big to illustrate how it should be used. Now a reader has to go through that PR, sift through non-relevant parts, read it, and figure out what's the idea here.
  2. A PR until it's merged is a living thing, can still change.
  3. There's a potential bitrot possibility in case yet another github organisation migration happens, github.com dissapearance etc.

Could you provide a self-contained example what do you mean here?

Additionally that PR does not show any errors' conversion.

Do you mean using exceptions as a replacement for Except e m a here?

@Jimbo4350 Jimbo4350 force-pushed the jordan/adr-8-RIO-in-cardano-cli branch from 8ad8bca to d6a6d03 Compare February 3, 2025 16:55
Comment on lines 82 to 84
Copy link
Collaborator

@carbolymer carbolymer Feb 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. The purpose is still not explained. Why do we need CustomCliException, and not just use fromEitherIO on our ExceptT err m a functions? You'd need to just provide instance Exception err and that should be enough I think.

  2. Why the handler needs to pattern match exceptions? They're rendered and logged in the same way.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. We need a callstack and the Error error constraint enforces a pretty rendering for the exception.
  2. Fair point it is also of the type SomeException.

Copy link
Collaborator

@carbolymer carbolymer Feb 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. We need a callstack and the Error error constraint enforces a pretty rendering for the exception.

I see. But that will only give you a call stack up to the place where the CustomCliException was used to wrap our current error type.

As an alternative I was thinking about this solution:

  1. Adding HasCallStack to each of our error constructors
  2. Add hand-written Exception instance to render callstack
-- | Our standard error clas
class Error e where
  prettyError :: e -> Text

-- | our typical CLI error type
data CmdError
  = HasCallStack => CmdOtherError Text

-- | Needs standalone deriving instance
deriving instance Show CmdError

-- | already existing Error instance
instance Error CmdError where
  prettyError (CmdOtherError e) = "CmdError: " <> e

-- | Needs manually written Exception instance to print callstack
instance Exception CmdError where
  displayException ce@(CmdOtherError _) = 
    T.unpack (prettyError ce) <> "\n" <> prettyCallStack callStack

The benefit is that you can write

  -- topLevelRunCommand :: ExampleClientCommand 
  --                    -> ExceptT ExampleClientCommandErrors IO ()
  fromEitherIO . runExceptT $ 
    topLevelRunCommand cliCmd
...
  throwIO $
    FileError "dummy.file" ()

instead of

  -- topLevelRunCommand :: ExampleClientCommand 
  --                    -> ExceptT ExampleClientCommandErrors IO ()
  fromEitherIO . runExceptT . firstExceptT CustomCliException $ 
    topLevelRunCommand cliCmd
...
  throwIO $
    CustomCliException $
      FileError "dummy.file" ()

which is a bit simpler and still conveys the same error information.

The downside is that it requires a lot more changes through the codebase. Eventually we can get to that point, but as an interim solution your CustomCliException is fine here. 👍🏻

Copy link
Collaborator Author

@Jimbo4350 Jimbo4350 Feb 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which is a bit simpler and still conveys the same error information.

I'm happy to go with this. The idea of the CustomCliException type was to remove the possibility of forgetting to implement an Error instance or including a HasCallStack constraint. However we are big boys and what you proposed works for me.

Copy link
Collaborator

@palas palas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good overall. Just a few suggestions for improving clarity and typos

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would hide the env parameter of the RIO now that it is easy, unless we are sure it is going to remain (), and even if we are. Maybe with a type synonym. Something like: type CIO = RIO (). That way if we eventually want to add an environment we won't have to change every type signature

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like type EmptyEnvironment = ()? We will eventually use it in logging.

Copy link
Collaborator

@carbolymer carbolymer Feb 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't bother with hiding it. I think we can just write function types for any environment e.g. RIO e a.

I'm wondering if maybe we could put stuff like LocalNodeConnectInfo for online commands, or environment variables into environment. So an online command using environment variables would have type like for example:

doSomeCliCommand 
  :: Has LocalNodeConnectInfo e
  => Has EnvCli e
  => HasCallStack
  => Foo -> Bar -> RIO e ()

class Has a e where
  obtain :: e -> a

But that's out of scope for this ADR.

Besides that, maybe it be useful to have a type synonym providing HasCallStack everywhere? e.g.

-- CIO, like Cardano IO?
type CIO e a = HasCallStack => RIO e a

and use it instead of RIO? It's a slight inconvenience to remember to put it everywhere.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if maybe we could put stuff like LocalNodeConnectInfo for online commands, or environment variables into environment.

100%. That is the point of the Has classes 👍 .

besides that, maybe it be useful to have a type synonym providing HasCallStack everywhere?

Good suggestion, yes we should do this.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, so we have traits. That works too

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just from the point of the document structure. I notice there are essentially three pros and cons sections. One in Proposed Solution, one in Exception Handling Mechanism:, and one disguised as Consequences. Would it make sense to unify them?

Copy link
Collaborator

@carbolymer carbolymer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work, LGTM. Can you let me know what do you think of:

  1. #60 (comment)
  2. #60 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Better composability i.e no more errors that wrap errors (see above).
- Better composability i.e no more errors that wrap errors (see above).
- Less bookeping required: no more wrapping low-level errors into high-level errors

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the point above already captures this

@Jimbo4350 Jimbo4350 force-pushed the jordan/adr-8-RIO-in-cardano-cli branch from a0b3ef3 to e15e5fa Compare February 10, 2025 15:15
@Jimbo4350 Jimbo4350 force-pushed the jordan/adr-8-RIO-in-cardano-cli branch from e15e5fa to b02b780 Compare February 10, 2025 15:19
@Jimbo4350 Jimbo4350 merged commit 5360329 into main Feb 10, 2025
1 check failed
@Jimbo4350 Jimbo4350 deleted the jordan/adr-8-RIO-in-cardano-cli branch February 10, 2025 15:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants