Apologies if this has been asked/answered many times over already -- I am having a hard time formulating what the problem actually is and thus didn't really know what to search for.
Essentially, I have a class I have defined such that :
class (MonadIO m) => Logger m where ...
And then I have a type (I want to say type synonym but I am not sure if this is the right 'term' ) :
type ResourceOpT r m a = StateT (ResourceCache r) m a
Why is it that this instance is perfectly valid :
instance (MonadIO m) => Logger ( StateT s m )
But not this one (I guess the first one is more abstract/preferrable but I'm trying to understand why) :
instance (MonadIO m) => Logger ( ResourceOpT r m )
Shouldn't both be equivalent by virtue of how I have defined ResourceOpT
? Specifically, the error I am getting is :
The type synonym 'ResourceOpT' should have 3 arguments, but has been given 2 In the instance declaration for 'Logger (ResourceOpT r m)'
I have a feeling what I am doing 'should' conceptually work but either my syntax is wrong or there is something (maybe a language extension) that I am missing or should be enabling for this to work.
Regardless, I would be interested to get your input and learn why this is wrong and also why I should/should not be doing that.
Thanks in advance.
The error reads:
The type synonym
ResourceOpT
should have 3 arguments
A type synonym (defined with type
; you have the right term!) must be applied to the same number of type arguments as the number of parameters in its definition. That is, it’s sort of a “macro” for types, which is just substituted with its definition; it can’t be partially applied like a function can. In your case, ResourceOpT
demands three arguments:
type ResourceOpT r m a = StateT (ResourceCache r) m a
-- ^ ^ ^
This restriction makes it possible to do type inference with higher-kinded types, that is, things that abstract over type constructors like Monad
and Foldable
. Allowing type synonyms to be partially applied would mean that the compiler couldn’t deduce things like (m Int
= Either String a
) ⇒ (m
= Either String
, a
= Int
).
There are a few solutions. One is to start by directly addressing what the compiler is talking about, and change the number of parameters in the definition of ResourceOpT
:
type ResourceOpT r m = StateT (ResourceCache r) m
-- ^ ---------- no ‘a’ ----------^
Then, entering this code:
instance (MonadIO m) => Logger ( ResourceOpT s m )
Produces this different message:
Illegal instance declaration for
Logger (ResourceOpT s m)
(All instance types must be of the form
(T t1 ... tn)
whereT
is not a synonym. UseTypeSynonymInstances
if you want to disable this.)
If you use the -XTypeSynonymInstances
compiler flag or {-# LANGUAGE TypeSynonymInstances #-}
pragma in a source file, it allows making an instance for the type that a synonym expands to. This produces yet another message:
Illegal instance declaration for
Logger (ResourceOpT s m)
(All instance types must be of the form(T a1 ... an)
wherea1 ... an
are distinct type variables, and each type variable appears at most once in the instance head. UseFlexibleInstances
if you want to disable this.)
FlexibleInstances
relaxes some restrictions on instances you can make. It shows up pretty often when writing certain kinds of code with monad transformers. Adding it, this code is accepted. What you’ve done here is make an instance of your Logger
class for StateT s m
for all s
and m
, provided that m
is in MonadIO
. If anyone wants to make a Logger
instance for a different specialisation of StateT
to something other than ResourceCache
, then it will be rejected, or they’ll have to jump through some dubious hoops with overlapping instances.
One alternative that doesn’t require these extensions is to make a newtype
instead of a type synonym:
newtype ResourceOpT r m a = ResourceOpT
{ getResourceOpT :: StateT (ResourceCache r) m a }
A newtype
is, well, a new type, not a synonym. In particular, it’s a zero-cost wrapper for another type: same representation but different typeclass instances.
Doing this, you can write or derive instances of Applicative
, Functor
, Monad
, MonadIO
, MonadState (ResourceCache r)
, and so on, for the concrete type constructor ResourceOpT
, just like all the other transformers in transformers
like StateT
, ReaderT
, and so on. You can also partially apply the ResourceOpT
constructor, because it’s not a type
synonym.
And in general, the reason to have a Logger
class is that you want to write code generic in the type of logger, because you have multiple different types that could be instances. But especially if ResourceOpT
is the only one, then you can also do away with the class and write code in the concrete ResourceOpT
, or a polymorphic m
with a constraint such as MonadState (ResourceCache r) m
. In general, a function parameter or polymorphic function is preferable to adding a new typeclass; however, without the details of the class definition and use case, it’s hard to say whether & how yours ought to be refactored.