Skip to content

snoyberg/trio

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

trio

I wrote this because John De Goes nerd sniped me :)

This is like the RIO monad: Reader + IO. In RIO, all exceptions are unchecked exceptions. There are valid reasons for this, based on how the GHC runtime system handles exceptions and async exceptions.

For scalaz, John recently discussed the idea of IO being a bifunctor: one type parameter for the types of exceptions, the other for the result. In discussions at LambdaConf Winter Retreat 2018, we discussed whether this idea could apply to Haskell. I pointed out that it couldn't, because IO in Haskell means "it can throw any exception type it wants." But John made an argument that I couldn't shake: perhaps we could make things work. This library is an experiment in that direction.

NOTE I just said experiment. This is nothing more than an experiment. I'm still using RIO and recommending it for production code. This is pure exploration.

As said, IO is scalaz will become a bifunctor. RIO is a profunctor: the r environment variable is contravariant, and the result value covariant. Trio is a trifunctor, with the reader being contravariant, and the exception and result being covariant. So we have:

newtype Trio r e a

The semantics of this are as follows: the type of e is the type of checked exceptions inside the action. These are actions that are intended to be caught. However, other exceptions may occur. In particular:

  • As is always the case in Haskell code, an asynchronous exception can occur anywhere.
  • By using the throwUnchecked function

Under the surface, this library is using unsafe shenanigans to make things work. That is theoretically hidden away entirely by hiding the internal interface.

From typeclasses

Exceptions are intended to have helper From typeclasses for conversions. For example:

class FromIOException e where
  fromIOException :: E.IOException -> e

instance FromIOException E.IOException where
  fromIOException = id
instance FromIOException E.SomeException where
  fromIOException = E.toException

This allows us to more easily coalesce different exception types. But more importantly that convenience, it allows us to write more useful with-style functions, e.g.:

withBinaryFile
  :: FromIOException e
  => FilePath
  -> IO.IOMode
  -> (IO.Handle -> Trio r e a)
  -> Trio r e a

Notice how the inner function is free to dictate a different exception type if desired.

NOTE It may make more sense to use a lensy Prism here instead.

Catch vs cleanup

Like the safe-exceptions library, this library is designed around proper handling of async exceptions. To make this clear, we distinguish between two cases of exception handling:

  • Catch-and-recover
  • Cleanup-and-rethrow

The former applies to functions like catch and try, which prevent exception propagation. Those apply exclusively here to the checked exception type (the second parameter to Trio).

The latter applies to all exception types: checked, unchecked, and asynchronous. These are functions like bracket, finally, and onException, which will rethrow the generated exception.

Usage of Void

Like the scalaz implementation, the idea here is to indicate that all exceptions have been handled by placing a Void value as the checked exception type.

Example usage

For now, just check out the test suite.

Why not ExceptT

I claim that Trio has the following advantages over ExceptT:

  • It's more efficient by avoid the Either wrapping and pattern matching
  • By design, it only has one method for exceptions to be reported, as opposed to ExceptT allowing exceptions to appear in either the Left value or runtime exceptions. This makes it much easier and safer to implement many functions like concurrently.
  • Hopefully by documenting it correctly from the start, it can make clear that while this type can capture checked exceptions explicitly, unchecked exceptions are still a reality.

Why the ReaderT bit?

All of the arguments from the RIO data type still apply: the common case in most applications is having some environment passed around (like config values), and we should optimize for that common case.

About

Crazy experiment, ignore unless you know better

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published