3

To elaborate, it is often possible to provide default implementations for type class instance functions, but I wonder if it is also possible to provide default implementations for type class instances of other type classes.

For instance, say I'm implementing type class Y and I want all instances a of Y to satisfy X a for some other type class X. Initially I was trying to do this by writing instance Y a => X a where ..., but saw this was not really possible (Haskell Constraint is no smaller than the instance head). However, unlike in the more general case described in the other question where there might be more than one type class constraint, I've just got one class constraint in my case, so I figure there might be a way to do this at the class definition level, maybe using a Haskell language extension.

The method described in the other question doesn't seem to work too smoothly - let's say X is actually Ord. Wrapping Ord by some newtype prevents direct usage of Ord functions on the original type.

Iceland_jack
  • 6,848
  • 7
  • 37
  • 46
bbarker
  • 11,636
  • 9
  • 38
  • 62
  • Can't you just derive an `Ord` instance for your newtype? – melpomene Jan 27 '19 at 20:59
  • If you want such constraint, you can define it as `class X a => Y a where ...`, so now all types `a` that implement `Y`, should implement `X` as well. But perhaps I'm misunderstanding it. – Willem Van Onsem Jan 27 '19 at 21:00
  • @melpomene I think my quick experimentation suggested that if I have `newtype Foo a = Foo a`, then if I derive `instance Ord (Foo a)`, while I could use `Ord` functions on `Foo a` values, I can't use them directly on values of type `a`. – bbarker Jan 27 '19 at 21:04
  • @WillemVanOnsem I think that what you suggest is going in the other direction; I want, given class definition `Y`, for all `a` satisfying `Y a`, to also have an `X a` (without the implementer of the `a` type needing to do any additional work). – bbarker Jan 27 '19 at 21:05

2 Answers2

9

The usual trick here is to define a newtype wrapper with the instance you want. See WrappedMonoid for an example of this. In your case:

newtype WrappedY a = WrapY { unwrapY :: a }

instance Y a => X (WrappedY a) where 
  -- your default implementation here

Then, a type that has an instance of Y can derive its instance of X using the new DerivingVia extension.

{-# LANGUAGE DerivingVia #-}

data SomeType = ...
  deriving X via WrappedY SomeType

instance Y SomeType where
  -- your implementation here
Alec
  • 31,829
  • 7
  • 67
  • 114
  • This is nice, thanks. I was able to avoid the record syntax in my case as well, eg.g just `WrapY a` for the newtype. – bbarker Jan 27 '19 at 21:14
  • 1
    @bbarker Don't do that. It would be very unusual to have a `newtype` like this that isn't a record. After defining it as a record, you can choose to use it in both record syntax and normal syntax. Very importantly, the accessor doesn't have to be defined explicitly, either. If you look through the `base` package you'll see find a *ton* of `newtype`s that don't outright *need* to be records but are. – HTNW Jan 27 '19 at 21:45
  • @HTNW could you elaborate on why/how it is useful to have a `newtype` defined without an explicit constructor in record syntax, or maybe a particular link to to source in `base` where it is happening? And also maybe on why it is a bad idea to not use record syntax in this case, though maybe the first question will answer the latter. – bbarker Jan 27 '19 at 23:25
  • 1
    @bbarker I think the point that @HTNW is trying to make is that the _convention_ is to use record syntax and that if you intend on having other people read this code, they'll probably have an easier time if you follow that convention. It's purely a point about style. As for a link, `WrappedMonoid` from my post is an example in `base`. Ditto far any of the other `Wrapped*` types. – Alec Jan 28 '19 at 00:20
  • 2
    @bbarker I suspect the misunderstanding here is this: you think that data types declared with record syntax must then always use record syntax. But it is not so. Even using Alec's definition from this answer, `WrapY "hi"` is a perfectly good value of type `WrappedY String`, and `f (WrapY x) = x ++ "foo"` is a perfectly good pattern match, producing a function of type `WrappedY String -> String`, even though neither of these used explicit record syntax. So using record syntax in the data declaration gives you an unwrapping function for free, and doesn't prevent any otherwise valid uses. – Daniel Wagner Jan 28 '19 at 13:07
5

The newtype wrapper + DerivingVia suggestion in Alec's answer works nicely if your user has GHC 8.6+ available (DerivingVia is a fairly new extension). In case that isn't an option, you can do something like -

  1. Provide a method like implXViaY which provides a method implementation for X via Y. For example, this is sometimes done with Applicative instances in the form of (<*>) = ap where ap is based on the implementation of >>= from Monad, or with Functor with fmap = liftA where liftA is based on the implementation of (<*>) in Applicative. This requires no extensions.

  2. Provide a Template Haskell function to automatically use the default definitions in case the type class has a lot of methods (this is basically an extension of 1). So something like makeXviaY ''MyType for the user. This requires the client to enable TemplateHaskell.

typesanitizer
  • 2,505
  • 1
  • 20
  • 44