-
-
Notifications
You must be signed in to change notification settings - Fork 2
Troubleshooting
Compiler errors given by oops
can be a somewhat obscure. This page is to document those errors and how to respond to them.
example1 :: ExceptT Int IO Int
example1 = OO.runOopsInExceptT @Int (OO.throw True)
------
src/Examples.hs:2:39: error:
• Uh oh! I couldn't find Bool inside the variant!
If you're pretty sure I'm wrong, perhaps the variant type is ambiguous;
could you add some annotations?
• In the second argument of ‘OO.runOopsInExceptT’, namely
‘(OO.throw True)’
In the expression: OO.runOopsInExceptT @Int (OO.throw True)
In an equation for ‘example1’:
example1 = OO.runOopsInExceptT @Int (OO.throw True)
|
334 | example1 = OO.runOopsInExceptT @Int (OO.throw True)
| ^^^^^^^^^^^^^
This means that the context of the expression (OO.throw True)
does not have Bool
in the variant's type list. You can find out what types are in the type list by placing a type hole (for example _u
) in the expression's place. Doing so yields the following:
example1 :: ExceptT Int IO Int
example1 = OO.runOopsInExceptT @Int _u
-----
src/Examples.hs:2:38: error:
• Found hole: _u :: ExceptT (Variant '[Int]) IO Int
Or perhaps ‘_u’ is mis-spelled, or not in scope
• In the second argument of ‘OO.runOopsInExceptT’, namely ‘_u’
In the expression: OO.runOopsInExceptT @Int _u
In an equation for ‘example1’:
example1 = OO.runOopsInExceptT @Int _u
• Relevant bindings include
example1 :: ExceptT Int IO Int (bound at Examples.hs:2:1)
|
334 | example1 = OO.runOopsInExceptT @Int _u
| ^^
We are trying to create a value of type Variant '[Int]
from a value of type Bool
which won't work.
In the context of error handling, this means the context does not handle errors of type Bool
, but an error of type Bool
is thrown. oops
being a library for type-safe error handling does not allow code to have unhandled errors as that represents a bug in the code.
You can fix this error by:
- making the context handle this error
- handle this error here
- modify the expression to not throw this error
example2 :: ()
=> Monad m
=> CouldBe e Text
=> ExceptT (Variant e) m ()
example2 = OO.throw @Int 0
-----
src/Examples.hs:5:12: error:
• Could not deduce (CouldBe e Int)
arising from a use of ‘OO.throw’
from the context: (Monad m, OO.CouldBeF e Text)
bound by the type signature for:
example2 :: forall (m :: * -> *) (e :: [*]).
(Monad m, OO.CouldBeF e Text) =>
ExceptT (Variant e) m ()
at src/Examples.hs:(5,1)-(5,29)
• In the expression: OO.throw @Int 0
In an equation for ‘example2’: _example = OO.throw @Int 0
|
337 | example2 = OO.throw @Int 0
| ^^^^^^^^^^^^^^^
This means that the context of the expression (OO.throw True)
does not have Bool
in
the variant's type list e
. The type list e
is parametric, so the way to specify
the type list e
contains a type is to use the CouldBe
constraint.
The error already tells you what types are known to be in the e
type list via the
constraints that have been reported for example2
.
The constraints list shows CouldBe e Text
, so we know the typelist e
contains Text
but
there is no CouldBe e Int
.
In the context of error handling, this error means that the function example2
declares that
it throws Text
and nothing else, but internally it calls a function that throws Int
without handling it.
You can fix this error by:
- declaring
examples2
also throwsInt
by addingCouldBe e Int
to the constraint of the function - handle the
Int
error so that it doesn't propagate out of the function - modify the expression to not throw
Int
example3 :: ExceptT Int IO ()
example3 = OO.throw @Int 0
-----
src/Examples.hs:1:12: error:
• Couldn't match type ‘Int’ with ‘OO.VariantF Identity e0’
arising from a functional dependency between:
constraint ‘MonadError (Variant e0) (ExceptT Int IO)’
arising from a use of ‘OO.throw’
instance ‘MonadError e (ExceptT e m)’ at <no location info>
• In the expression: OO.throw @Int 0
In an equation for ‘_example’: _example = OO.throw @Int 0
|
334 | example3 = OO.throw @Int 0
| ^^^^^^^^^^^^^^^
Oops uses ExceptT (Variant e)
to propagate errors. The type expression VariantF Identity e0
is equivalent to Variant e0
.
This error is saying that Variant e
and Int
are not the same type. You get this error when
trying to call code that uses oops
for error handling from code that uses Vanilla ExceptT
.
You can fix the error by using the runOopsInExceptT
function like this:
example3 :: ExceptT Int IO ()
example3 = OO.runOopsInExceptT $ OO.throw @Int 0
To understand what's happening here you can replace the throwing expression with a type hole _u
:
example3 :: ExceptT Int IO ()
example3 = OO.runOopsInExceptT _u
-----
src/Examples.hs:2:32: error:
• Found hole: _u :: ExceptT (Variant '[Int]) IO ()
Or perhaps ‘_u’ is mis-spelled, or not in scope
• In the first argument of ‘OO.runOopsInExceptT’, namely ‘_u’
In the expression: OO.runOopsInExceptT _u
In an equation for ‘_example’: _example = OO.runOopsInExceptT _u
• Relevant bindings include
example3 :: ExceptT Int IO () (bound at src/Examples.hs:334:1)
|
334 | example3 = OO.runOopsInExceptT _u
| ^^
The type of _u
is required to be ExceptT (Variant '[Int]) IO ()
. Notice that the Variant
has the type list '[Int]
. Expressions in located where the type hole is currently are allowed to throw Int
and nothing else.
data Error = Error1 Int | Error2 Bool
foo :: ()
=> e `CouldBe` Int
=> e `CouldBe` Bool
=> MonadError (Variant e) m
=> m ()
foo = OO.throw @Int 0
example4 :: ExceptT Error IO ()
example4 = OO.runOopsInExceptT foo
-----
src/Examples.hs:2:34: error:
• Uh oh! I couldn't find Int inside the variant!
If you're pretty sure I'm wrong, perhaps the variant type is ambiguous;
could you add some annotations?
• In the second argument of ‘($)’, namely ‘OO.throw @Int 0’
In the expression: OO.runOopsInExceptT $ OO.throw @Int 0
In an equation for ‘example4’:
example4 = OO.runOopsInExceptT $ OO.throw @Int 0
|
336 | example4 = OO.runOopsInExceptT $ OO.throw @Int 0
| ^^^^^^^^^^^^^^^
This error can happen when interfacing between calling oops
code from non-oops
code. Here we are
correctly using runOopsInExceptT
, however, the expression inside runOopsInExceptT
may only throw
Error
. This is inferred from the context of the call to runOopsInExcept
, which has the type
ExceptT Error IO ()
.
Let's put in a type hole to find out:
data Error = Error1 Int | Error2 Bool
foo :: ()
=> e `CouldBe` Int
=> e `CouldBe` Bool
=> MonadError (Variant e) m
=> m ()
foo = OO.throw @Int 0
example4 :: ExceptT Error IO ()
example4 = OO.runOopsInExceptT _u
src/Examples.hs:11:32: error:
• Found hole: _u :: ExceptT (Variant '[Error]) IO ()
Or perhaps ‘_u’ is mis-spelled, or not in scope
• In the first argument of ‘OO.runOopsInExceptT’, namely ‘_u’
In the expression: OO.runOopsInExceptT _u
In an equation for ‘example4’: _example = OO.runOopsInExceptT _u
• Relevant bindings include
example4 :: ExceptT Error IO () (bound at src/Examples.hs:11:1)
|
336 | example4 = OO.runOopsInExceptT _u
| ^^
This means we can only throw Error
at that location but foo
throws both Int
and Bool
.
That's okay. We can catch the Int
and the Bool
and wrap it in Error
like this:
data Error = Error1 Int | Error2 Bool
foo :: ()
=> e `CouldBe` Int
=> e `CouldBe` Bool
=> MonadError (Variant e) m
=> m ()
foo = OO.throw @Int 0
example4 :: ExceptT Error IO ()
example4 = OO.runOopsInExceptT $ do
foo
& OO.catch @Int (OO.throw . Error1)
& OO.catch @Bool (OO.throw . Error2)
foo :: ExceptT Int IO ()
foo = pure ()
example5 :: ()
=> e `CouldBe` Int
=> ExceptT (Variant e) IO ()
example5 = foo
-----
src/Examples.hs:7:12: error:
• Couldn't match type ‘Error’ with ‘OO.VariantF Identity e’
Expected type: ExceptT (Variant e) IO ()
Actual type: ExceptT Int IO ()
• In the expression: foo
In an equation for ‘_example’: _example = foo
• Relevant bindings include
example5 :: ExceptT (Variant e) IO ()
(bound at src/Examples.hs:7:1)
|
341 | example5 = foo
| ^^^
This is error is saying that Error
and Variant e
aren't the same type. This error can occur
when call non-oops
code from within oops
code.
This can be solved by wrapping the call to foo
in functions that convert the type ExceptT Int IO ()
to e `CouldBe` Int => ExceptT (Variant e) IO ()
like this:
foo :: ExceptT Int IO ()
foo = pure ()
example5 :: ()
=> e `CouldBe` Int
=> ExceptT (Variant e) IO ()
example5 =
lift (runExceptT foo)
& OO.onLeft OO.throw
The role of each function is as follows:
-
runExceptT
: ConvertsExceptT Int IO ()
toIO (Either Int ())
-
lift
: ConvertsIO (Either Int ())
toe `CouldBe` Int => ExceptT (Variant e) IO (Either Int ())
. Notice we are now in the same monadic context asexample5
. Now we just need to handle theEither Int
in the return type of the monad. -
onLeft
: Extracts theInt
fromEither Int ()
leaving only()
.onLeft
takes a function which specifies what to do with theInt
. -
throw
: Throws theInt
as an error. Given that we are already in the correct monadic context thate `CouldBe` Int => ExceptT (Variant e) IO ()
, thethrow
and therefore the entireexample5
function compiles.
As a general rule, to call non-oops
code f
from oops
code g
, you will need to take f
and transform it such that all the errors are represented in the return type of the based monad that both f
and g
share then you lift
to get the expression to be in the same monad as g
, then you inspect the value for the errors embedded within and throw them.