Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,4 @@ jobs:

- name: Test
run: |
echo 'No tests'
cabal test
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,15 @@ cabal.project.local
.ghc.environment.*
.HTF/
.hie/
ghcid.txt

# Stack
.stack-work/
stack.yaml.lock

# Nix
shell.nix

Comment on lines +25 to +33
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I hope you don't mind me adding these. I like to use nix-shell to pull in all the project dependencies (such as ghc, and cabal) and use ghcid for development. I didn't want to clutter up anyone else's workflow so I added the files I use to .gitignore. If this is annoying I can just stage my files more carefully and remove this.

### IDE/support
# Vim
[._]*.s[a-v][a-z]
Expand Down
13 changes: 13 additions & 0 deletions eio.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,19 @@ library
import: common-options
hs-source-dirs: src
exposed-modules: EIO
, EIO.TypeErrors

test-suite eio-doctest
import: common-options
type: exitcode-stdio-1.0
hs-source-dirs: test
main-is: Doctest.hs

build-depends: eio
, doctest
, Glob

ghc-options: -threaded

executable readme
import: common-options
Expand Down
46 changes: 44 additions & 2 deletions src/EIO.hs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ module EIO
-- * Basic API
, throw
, catch
, unsafeLiftIO
, tryLiftIO
-- * QualifieDo interface
, return
, (>>=)
Expand All @@ -26,13 +28,17 @@ module EIO

import Prelude hiding (return, (>>), (>>=))

import Control.Exception (Exception)
import Control.Exception (Exception, try)
import Data.Coerce (coerce)
import Data.Kind (Type)
import EIO.TypeErrors (DisallowUnhandledExceptions)

import qualified GHC.IO as IO
import qualified Prelude

-- $setup
-- >>> data MyErr = MyErr deriving (Show)
-- >>> instance Exception MyErr

{- | Main type for 'IO' that tracks exceptions on the
type-level. Simply wraps 'IO' and adds exceptions meta-information.
Expand Down Expand Up @@ -60,11 +66,27 @@ safeMain = EIO.do
... your code ...
@

>>> :{
runEIO $ EIO.do
throw MyErr `catch` (\MyErr -> unsafeLiftIO $ putStrLn "handled error")
unsafeLiftIO $ putStrLn "ran action"
>>> :}
handled error
ran action

>>> EIO.runEIO $ EIO.throw MyErr >> EIO.return ()
...
... • The 'runEIO' handler requires that all exceptions in 'EIO' to be handled.
... The action 'runEIO' is applied to throws the following unhandled exceptions:
... • MyErr
...

@since 0.0.0.0
-}
runEIO :: EIO '[] () -> IO ()
runEIO :: (DisallowUnhandledExceptions excepts) => EIO excepts () -> IO ()
runEIO = coerce


{- | Wrap a value into 'EIO' without throwing any exceptions.

@since 0.0.0.0
Expand Down Expand Up @@ -92,6 +114,26 @@ type family (<>) (xs :: [Type]) (ys :: [Type]) :: [Type] where
xs <> '[] = xs
(x ': xs) <> ys = x ': (xs <> ys)

{- | Allows one to lift an IO action into EIO, but you are telling the compiler
that there are no exceptions in your IO action. The safety of this function is
contingent on the user keeping their promise of exception free code, which is why
this function is labelled as unsafe.

@since 0.0.1.1
-}
unsafeLiftIO :: IO a -> EIO '[] a
unsafeLiftIO = EIO


{- | A safer version of `unsafeLiftIO` this function first tries the action
and forces the caller to handle the exception purely before before proceeding
in EIO with a clean exception state.

@since 0.0.1.1
-}
tryLiftIO :: (Exception e) => IO a -> EIO '[] (Either e a)
tryLiftIO = EIO . try

{- | Throw exception.

@since 0.0.0.0
Expand Down
24 changes: 24 additions & 0 deletions src/EIO/TypeErrors.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}

module EIO.TypeErrors
(DisallowUnhandledExceptions)
where

import Data.Kind (Type, Constraint)
import GHC.TypeLits

type family DisallowUnhandledExceptions (excepts :: [Type]) :: Constraint where
DisallowUnhandledExceptions '[] = ()
DisallowUnhandledExceptions excepts =
TypeError
( 'Text "The 'runEIO' handler requires that all exceptions in 'EIO' to be handled."
':$$: 'Text "The action 'runEIO' is applied to throws the following unhandled exceptions:"
Comment on lines +17 to +18
Copy link
Copy Markdown
Author

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 text as similar to your suggestion as possible.

':$$: ShowTypeList excepts
)

type family ShowTypeList (xs :: [Type]) :: ErrorMessage where
ShowTypeList '[] = 'Text ""
ShowTypeList (x ': xs) = 'Text " • " ':<>: ('ShowType x) ':$$: (ShowTypeList xs)
20 changes: 20 additions & 0 deletions test/Doctest.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module Main (main) where

import EIO

import System.FilePath.Glob (glob)
import Test.DocTest (doctest)

main :: IO ()
main = do
sourceFiles <- glob "src/**/*.hs"
doctest
$ "-XInstanceSigs"
: "-XNoImplicitPrelude"
: "-XOverloadedStrings"
: "-XScopedTypeVariables"
: "-XTypeApplications"
: "-XDerivingStrategies"
: "-XGeneralizedNewtypeDeriving"
: "-XQualifiedDo"
: sourceFiles