I'm attempting to rewrite a simple interpreter from a transformers-based monad stack to effects based on freer, but I'm running up against a difficulty communicating my intent to GHC's type system.
I am only using the State
and Fresh
effects at present. I'm using two states and my effect runner looks like this:
runErlish g ls = run . runGlobal g . runGensym 0 . runLexicals ls
where runGlobal = flip runState
runGensym = flip runFresh'
runLexicals = flip runState
On top of this, I have defined a function FindMacro with this type:
findMacro :: Members [State (Global v w), State [Scope v w]] r
=> Arr r Text (Maybe (Macro (Term v w) v w))
All of this so far works perfectly fine. The problem comes when I try to write macroexpand2
(well, macroexpand1, but i'm simplifying it so the question is easier to follow):
macroexpand2 s =
do m <- findMacro s
return $ case m of
Just j -> True
Nothing -> False
This produces the following error:
Could not deduce (Data.Open.Union.Member'
(State [Scope v0 w0])
r
(Data.Open.Union.FindElem (State [Scope v0 w0]) r))
from the context (Data.Open.Union.Member'
(State [Scope v w])
r
(Data.Open.Union.FindElem (State [Scope v w]) r),
Data.Open.Union.Member'
(State (Global v w))
r
(Data.Open.Union.FindElem (State (Global v w)) r))
bound by the inferred type for `macroexpand2':
(Data.Open.Union.Member'
(State [Scope v w])
r
(Data.Open.Union.FindElem (State [Scope v w]) r),
Data.Open.Union.Member'
(State (Global v w))
r
(Data.Open.Union.FindElem (State (Global v w)) r)) =>
Text -> Eff r Bool
at /tmp/flycheck408QZt/Erlish.hs:(79,1)-(83,23)
The type variables `v0', `w0' are ambiguous
When checking that `macroexpand2' has the inferred type
macroexpand2 :: forall (r :: [* -> *]) v (w :: [* -> *]).
(Data.Open.Union.Member'
(State [Scope v w])
r
(Data.Open.Union.FindElem (State [Scope v w]) r),
Data.Open.Union.Member'
(State (Global v w))
r
(Data.Open.Union.FindElem (State (Global v w)) r)) =>
Text -> Eff r Bool
Probable cause: the inferred type is ambiguous
Okay, I can add a Members
annotation to the type:
macroexpand2 :: Members [State (Global v w), State [Scope v w]] r
=> Text -> Eff r Bool
And now I get this:
Overlapping instances for Member (State [Scope v0 w0]) r
arising from a use of `findMacro'
Matching instances:
instance Data.Open.Union.Member'
t r (Data.Open.Union.FindElem t r) =>
Member t r
-- Defined in `Data.Open.Union'
There exists a (perhaps superclass) match:
from the context (Members
'[State (Global v w), State [Scope v w]] r)
bound by the type signature for
macroexpand2 :: Members
'[State (Global v w), State [Scope v w]] r =>
Text -> Eff r Bool
at /tmp/flycheck408QnV/Erlish.hs:(79,17)-(80,37)
(The choice depends on the instantiation of `r, v0, w0'
To pick the first instance above, use IncoherentInstances
when compiling the other instance declarations)
In a stmt of a 'do' block: m <- findMacro s
In the expression:
do { m <- findMacro s;
return
$ case m of {
Just j -> True
Nothing -> False } }
In an equation for `macroexpand2':
macroexpand2 s
= do { m <- findMacro s;
return
$ case m of {
Just j -> True
Nothing -> False } }
I was advised on irc to try forall r v w.
which made no difference. Out of curiosity i tried using IncoherentInstances
when compiling this code (I did not fancy checking out a fork of freer and playing) to see if perhaps it would give me a clue as to what was going on. It did not:
Could not deduce (Data.Open.Union.Member'
(State [Scope v0 w0])
r
(Data.Open.Union.FindElem (State [Scope v0 w0]) r))
arising from a use of `findMacro'
from the context (Members
'[State (Global v w), State [Scope v w]] r)
bound by the type signature for
macroexpand2 :: Members
'[State (Global v w), State [Scope v w]] r =>
Text -> Eff r Bool
at /tmp/flycheck408eru/Erlish.hs:(79,17)-(80,37)
The type variables `v0', `w0' are ambiguous
Relevant bindings include
macroexpand2 :: Text -> Eff r Bool
(bound at /tmp/flycheck408eru/Erlish.hs:81:1)
Note: there are several potential instances:
instance (r ~ (t' : r'), Data.Open.Union.Member' t r' n) =>
Data.Open.Union.Member' t r ('Data.Open.Union.S n)
-- Defined in `Data.Open.Union'
instance (r ~ (t : r')) =>
Data.Open.Union.Member' t r 'Data.Open.Union.Z
-- Defined in `Data.Open.Union'
In a stmt of a 'do' block: m <- findMacro s
In the expression:
do { m <- findMacro s;
return
$ case m of {
Just j -> True
Nothing -> False } }
In an equation for `macroexpand2':
macroexpand2 s
= do { m <- findMacro s;
return
$ case m of {
Just j -> True
Nothing -> False } }
So, this is where my understanding of freer's internals runs out and I have questions:
- Why is there an overlapping instance? I don't understand where that's coming from.
- What does IncoherentInstances actually do? Autoselection sounds pretty likely to cause hard-to-debug errors.
- How do I actually make use of findMacro within another function?
Cheers!
Type inference for extensible effects has been historically bad. Let's see some examples:
I'll try to explain the general issue and provide minimal code illustration. A self-contained version of the code can be found here.
On the most basic level (disregarding optimized representations),
Eff
is defined the following way:In other words,
Eff
is a free monad from an union of a list of functors.Union fs a
behaves like an n-aryCoproduct
. The binaryCoproduct
is likeEither
for two functors:In contrast,
Union fs a
lets us choose a functor from a list of functors:Let's implement the
State
effect as an example. First, we need to have aFunctor
instance forUnion fs
, sinceEff
only has aMonad
instance ifFunctor (Union fs)
.Functor (Union '[])
is trivial, since the empty union doesn't have values:Otherwise we prepend a functor to the union:
Now the
State
definition and the runners:We can already start writing and running our
Eff
programs, although we lack all the sugar and convenience:Now:
There are only two essential missing things here.
The first one is the monad instance for
Eff
which lets us usedo
-notation instead ofFree
andPure
, and also lets us use the many polymorphic monadic functions. We shall skip it here because it's straightforward to write.The second one is inference/overloading for choosing effects from lists of effects. Previously we needed to write
Here x
in order to choose the first effect,There (Here x)
to choose the second one, and so on. Instead, we'd like to write code that's polymorphic in effect lists, so all we have to specify is that some effect is an element of a list, and some hidden typeclass magic will insert the appropriate number ofThere
-s.We need a
Member f fs
class that can injectf a
-s intoUnion fs a
-s whenf
is an element offs
. Historically, people have implemented it in two ways.First, directly with
OverlappingInstances
:Second, indirectly, by first computing the index of
f
infs
with a type family, then implementinginj
with a non-overlapping class, guided byf
-s computed index. This is generally viewed as better because people tend to dislike overlapping instances.The
freer
library uses the second solution, whileextensible-effects
uses the first one for GHC versions older than 7.8, and the second for newer GHC-s.Anyway, both solutions have the same inference limitation, namely that we can almost always
Lookup
only concrete monomorphic types, not types that contain type variables. Examples in ghci:This works because there are no type variables in either
Maybe
or[Maybe, []]
.This one gets stuck because the
a
type variable blocks reduction.This works, because the only thing we know about arbitrary type variables is that they are equal to themselves, and
a
is equal toa
.In general, type family reduction gets stuck on variables, because constraint solving can potentially refine them later to different types, so GHC can't make any assumptions about them (aside from being equal to themselves). Essentially the same issue arises with the
OverlappingInstances
implementation (although there aren't any type families).Let's revisit
freer
in the light of this.GHC knows that we have a stack with a single effect, since
run
works onEff '[] a
. It also knows that that effect must beState s
. But when we writeget
, GHC only knows that it has aState t
effect for some fresht
variable, and thatNum t
must hold, so when it tries to compute thefreer
equivalent ofLookup (State t) '[State s]
, it gets stuck on the type variables, and any further instance resolution trips up on the stuck type family expression. Another example:This also fails because GHC needs to compute
Lookup (State s) '[State Bool]
, and although we know that the state must beBool
, this still gets stuck because of thes
variable.This works because the state type of
modify not
can be resolved toBool
, andLookup (State Bool) '[State Bool]
reduces.Now, after this large detour, I shall address your questions at the end of your post.
Overlapping instances
is not indicative of any possible solution, just a type error artifact. I would need more code context to pinpoint how exactly it arises, but I'm sure it's not relevant, since as soon asLookup
gets stuck the case becomes hopeless.IncoherentInstances
is also irrelevant and doesn't help. We need a concrete effect position index to be able to generate code for the program, and we can't pull an index out of thin air ifLookup
gets stuck.The problem with
findMacro
is that it hasState
effects with type variables inside the states. Whenever you want to usefindMacro
you have to make sure that thev
andw
parameters forScope
andGlobal
are known concrete types. You can do this by type annotations, or more conveniently you can useTypeApplications
, and writefindMacro @Int @Int
for specifyingv = Int
andw = Int
. If you havefindMacro
inside a polymorphic function, you need to enableScopedTypeVariables
, bindv
andw
using aforall v w.
annotation for that function, and writefindMacro @v @w
when you use it. You also need to enable{-# language AllowAmbiguousTypes #-}
for polymorphicv
orw
(as pointed out in the comments). I think though that in GHC 8 it's a reasonable extension to enable, together withTypeApplications
.Addendum:
However, fortunately new GHC 8 features let us repair type inference for extensible effects, and we can infer everything
mtl
can, and also some thingsmtl
can't handle. The new type inference is also invariant with respect to the ordering of effects.I have a minimal implementation here along with a number of examples. However, it's not yet used in any effect library that I know of. I'll probably do a write-up on it, and do a pull request in order to add it to
freer
.