Linking/Combining Type Classes in Haskell

2019-06-16 04:28发布

Say I have two type classes defined as follows that are identical in function but different in names:

class Monad m where
    (>>=)  :: m a -> (a -> m b) -> m b
    return :: a -> m a

class PhantomMonad p where
    pbind    :: p a -> (a -> p b) -> p b
    preturn  :: a -> p a

Is there a way to tie these two classes together so something that is an instance of PhantomMonad will automatically be an instance of Monad, or will instances for each class have to be explicitly written? Any insight would be most appreciated, thanks!

5条回答
smile是对你的礼貌
2楼-- · 2019-06-16 04:50

Is there a way to tie these two classes together so something that is an instance of PhantomMonad will automatically be an instance of Monad?

Yes, but it requires the slightly alarming language extensions FlexibleInstances and UndecidableInstances:

instance (PhantomMonad m) => Monad m where
  return = preturn
  (>>=)  = pbind

FlexibleInstances is not so bad, but the risk of undecidability is slightly more alarming. The issue is that in the inference rule, nothing is getting smaller, so if you combine this instance declaration with another similar one (like say the reverse direction), you could easily get the type checker to loop forever.

I'm generally comfortable using FlexibleInstances, but I tend to avoid UndecidableInstances without a very good reason. Here I agree with Don Stewart's suggestion that you'd be better off using Monad to begin with. But your question is more in the nature of a thought experiment, the answer is that you can do what you want without getting into an Oleg level of scariness.

查看更多
我欲成王,谁敢阻挡
3楼-- · 2019-06-16 04:55

That's an unusual design. Can you not just remove the PhantomMonad, since it is isomorphic to the other class.

查看更多
\"骚年 ilove
4楼-- · 2019-06-16 04:56

Though this doesn't really make sense, try

instance Monad m => PhantomMonad m where
    pbind = (>>=)
    preturn = return

(maybe with some compiler warnings deactivated).

查看更多
乱世女痞
5楼-- · 2019-06-16 05:02

Another solution is to use newtype. This isn't exactly what you want, but is often used in such cases.

This allows linking different ways of specifying the same structure. For example, ArrowApply (from Control.Arrow) and Monad are equivalent. You can use Kleisli to make an ArrowApply out of a monad, and ArrowMonad to make a monad out of ArrowApply.

Also, one-way wrappers are possible: WrapMonad (in Control.Applicative) forms an applicative out of a monad.

class PhantomMonad p where
    pbind    :: p a -> (a -> p b) -> p b
    preturn  :: a -> p a

newtype WrapPhantom m a = WrapPhantom { unWrapPhantom :: m a }

newtype WrapReal m a = WrapReal { unWrapReal :: m a }

instance Monad m => PhantomMonad (WrapPhantom m) where
  pbind (WrapPhantom x) f = WrapPhantom (x >>= (unWrapPhantom . f))
  preturn = WrapPhantom . return

instance PhantomMonad m => Monad (WrapReal m) where
  WrapReal x >>= f = WrapReal (x `pbind` (unWrapReal . f))
  return = WrapReal . preturn
查看更多
何必那么认真
6楼-- · 2019-06-16 05:16

Good answer: No, what you're hoping to do isn't really viable. You can write an instance that looks like it does what you want, possibly needing some GHC extensions in the process, but it won't work the way you you'd like it to.

Unwise answer: You can probably accomplish what you want using scary type-level metaprogramming, but it may get complicated. This really isn't recommended unless you absolutely need this to work for some reason.

Officially instances can't really depend on other instances, because GHC only looks at the "instance head" when making decisions, and class constraints are in the "context". To make something like a "type class synonym" here, you'd have to write what looks like an instance of Monad for all possible types, which obviously doesn't make sense. You'll be overlapping with other instances of Monad, which has its own problems.

On top of all that, I don't think such an instance will satisfy the termination check requirements for instance resolution, so you'd also need the UndecidableInstances extension, which means the ability to write instances that will send GHC's type checker into an infinite loop.

If you really want to go down that rabbit hole, browse around on Oleg Kiselyov's website a bit; he's sort of the patron saint of type-level metaprogramming in Haskell.

It's fun stuff, to be sure, but if you just want to write code and have it work, probably not worth the pain.

Edit: Okay, in hindsight I've overstated the issue here. Something like PhantomMonad works fine as a one-off and should do what you want, given the Overlapping- and UndecidableInstances GHC extensions. The complicated stuff starts up when you want to do anything much more complicated than what's in the question. My sincere thanks to Norman Ramsey for calling me on it--I really should have known better.

I still don't really recommend doing this sort of thing without good reason, but it's not as bad as I made it sound. Mea culpa.

查看更多
登录 后发表回答