I'm writing a type class for my pipes
library to define an abstract interface to Proxy
-like types. The type class looks something like:
class ProxyC p where
idT :: (Monad m) => b' -> p a' a b' b m r
(<-<) :: (Monad m)
=> (c' -> p b' b c' c m r)
-> (b' -> p a' a b' b m r)
-> (c' -> p a' a c' c m r)
... -- other methods
I'm also writing extensions for the Proxy
type that are of the form:
instance (ProxyC p) => ProxyC (SomeExtension p) where ....
... and I'd like these instances to be able to impose an additional constraint that if m
is a Monad
then p a' a b' b m
is a Monad
for all a'
, a
, b'
, and b
.
However, I have no clue how to cleanly encode that as a constraint either for the ProxyC
class or for the instances. The only solution I currently know of is to do something like encoding it in the method signatures of the class:
(<-<) :: (Monad m, Monad (p b' b c' c m), Monad (p a' a b' b m))
=> (c' -> p b' b c' c m r)
-> (b' -> p a' a b' b m r)
-> (c' -> p a' a c' c m r)
... but I was hoping there would be a simpler and more elegant solution.
Edit: And not even that last solution works, since the compiler does not deduce that (Monad (SomeExtension p a' a b' b m))
implies (Monad (p a' a b' b m))
for a specific choice of variables, even when given the following instance:
instance (Monad (p a b m)) => Monad (SomeExtension p a b m) where ...
Edit #2: The next solution I'm considering is just duplicating the methods for the Monad
class within the ProxyC
class:
class ProxyC p where
return' :: (Monad m) => r -> p a' a b' b m r
(!>=) :: (Monad m) => ...
... and then instantiating them with each ProxyC
instance. This seems okay for my purposes since the Monad
methods only need to be used internally for extension writing and the original type still has a proper Monad
instance for the downstream user. All this does is just expose the Monad
methods to the instance writer.
An instance of a class is an individual object which belongs to that class. In Haskell, the class system is (roughly speaking) a way to group similar types. (This is the reason we call them "type classes"). An instance of a class is an individual type which belongs to that class.
A constraint is a requirement that types used as type arguments must satisfy. For example, a constraint might be that the type argument must implement a certain interface or inherit from a specific class. Constraints are optional; not specifying a constraint on a parameter is equivalent to using a Object constraint.
Interface Type Constraint You can constrain the generic type by interface, thereby allowing only classes that implement that interface or classes that inherit from classes that implement the interface as the type parameter.
In a generic type or method definition, a type parameter is a placeholder for a specific type that a client specifies when they create an instance of the generic type.
a rather trivial way to do this is use a GADT to move the proof to the value level
data IsMonad m where
IsMonad :: Monad m => IsMonad m
class ProxyC p where
getProxyMonad :: Monad m => IsMonad (p a' a b' b m)
you will need to explicitly open the dictionary wherever you need it
--help avoid type signatures
monadOf :: IsMonad m -> m a -> IsMonad m
monadOf = const
--later on
case getProxyMonad `monadOf` ... of
IsMonad -> ...
the tactic of using GADTs to pass proofs of propositions is really very general. If you are okay using constraint kinds, and not just GADTs, you can instead use Edward Kmett's Data.Constraint
package
class ProxyC p where
getProxyMonad :: Monad m => Dict (Monad (p a' a b' b m))
which lets you defined
getProxyMonad' :: ProxyC p => (Monad m) :- (Monad (p a' a b' b m))
getProxyMonad' = Sub getProxyMonad
and then use a fancy infix operator to tell the compiler where to look for the monad instance
... \\ getProxyMonad'
in fact, the :-
entailment type forms a category (where the objects are constraints), and this category has lots of nice structure, which is to say it is pretty nice to do proofs with.
p.s. none of these snippets are tested.
edit: you could also combine the value level proofs with a newtype wrapper and not need to open GADTs all over the place
newtype WrapP p a' a b' b m r = WrapP {unWrapP :: p a' a b' b m r}
instance ProxyC p => Monad (WrapP p) where
return = case getProxyMonad of
Dict -> WrapP . return
(>>=) = case getProxyMonad of
Dict -> \m f -> WrapP $ (unWrapP m) >>= (unWrapP . f)
instance ProxyC p => ProxyC (WrapP p) where
...
I suspect, but obviously have not tested, that this implementation will also be relatively efficient.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With