3. MIGRATION IS PAINFUL
• Adding a superclass
• data Foo a = …
instance Monad Foo where …
-- No instance for Applicative Foo
• Removing a method
• instance Monad Foo where …
fail x = …
-- 'fail' is not a (visible) method of class
'Monad'
• Sometimes #ifdef s cannot be avoided 😵
4. ABSTRACT TYPECLASS
• A data type whose constructors are hidden is abstract
• data Foo = {- hidden -}
• A similar concept could be employed to type classes: A type class
whose methods are hidden
• class Foo a where {- hidden -}
5. AN EXAMPLE OF ABSTRACT TYPECLASS
• An example of abstract
typeclass is
GHC.TypeLits.KnownN
at
• Its method natSing is
hidden
• The user can use this
class via
natVal :: KnownNat
n => proxy n ->
Integer
6. THE MERIT OF BEING ABSTRACT
• The author of the class can freely change the definition of the
class
• e.g. KnownNat has changed its representation since GHC 8.2
(Integer → Natural)
• The user doesn't need to care if its representation changed, as
long as natVal doesn't change
7. MAKING ‘Monad’ CLASS ABSTRACT
• module MyMonad (MyMonad, (>>=), return, fail)
where
• class MyMonad m where
-- the methods are not exported!
bind :: m a -> (a -> m b) -> m b
return_ :: a -> m a
fail_ :: String -> m a
• (>>=) :: MyMonad m => m a -> (a -> m b) -> m b
(>>=) = bind
return :: MyMonad m => a -> m a
return = return_
fail :: MyMonad m => String -> m a
fail = fail_
8. THE PROBLEM WITH ABSTRACT TYPECLASSES
• newtype Foo a = …
instance MyMonad Foo where
{- … what can I write here? 🤔 -}
• Third-party cannot write instances of such classes…
• …without using
• default definitions
• GeneralizedNewtypeDeriving
• …and yes, DerivingVia 😎
9. USING DerivingVia TO WRITE INSTANCES
• module MyMonad (…, MyMonadV1(..), ImplV1(..)) where
• class MyMonad m where …
• class MyMonadV1 m where
-- public!
bindV1 :: m a -> (a -> m b) -> m b
returnV1 :: a -> m a
failV1 :: String -> m a
failV1 = error
• newtype ImplV1 m a = ImplV1 (m a)
• instance MyMonadV1 m => MyMonad (ImplV1 m) where
-- hypothetical code; needs InstanceSigs to work
bind = coerce (bindV1 @m)
return_ = coerce (returnV1 @m)
fail_ = coerce (failV1 @m)
10. USING DerivingVia TO WRITE INSTANCES
• Want to write an instance of MyMonad? Use DerivingVia!
• import MyMonad
• newtype Identity a = Identity a
deriving MyMonad
via ImplV1 Identity
• instance MyMonadV1 Identity where
bindV1 (Identity x) f = f x
returnV1 x = Identity x
11. CHANGING THE CLASS HIERARCHY
• Now suppose you want to refactor MyMonad class
• class Applicative m => MyMonad m where
bind :: m a -> (a -> m b) -> m b
• class MyMonad m => MyMonadFail m where
fail_ :: String -> m a
• Can we avoid breakage?
12. BREAKAGE CAN BE AVOIDED
• …if we change ImplV1's instances accordingly!
• MyMonadV1 is kept as is!
• instance MyMonadV1 m => Functor (ImplV1 m) where …
instance MyMonadV1 m => Applicative (ImplV1 m) where …
instance MyMonadV1 m => MyMonad (ImplV1 m) where …
instance MyMonadV1 m => MyMonadFail (ImplV1 m) where …
• -- a dirty hack is needed (unfortunately)
instance {-# OVERLAPPABLE #-} MyMonadV1 m => Functor m
where
fmap = liftM
instance {-# OVERLAPPABLE #-} MyMonadV1 m =>
Applicative m where
pure = returnV1; (<*>) = ap
13. WHEN TO USE THIS TECHNIQUE?
• When writing a new library
• When you are sure that you would want to change details in the future
• My library unboxing-vector uses a technique similar to this:
• class … => Unboxable a where
type Rep a
-- hidden methods:
unboxingFrom :: a -> Rep a
unboxingTo :: Rep a -> a
• Users can use a newtype wrapper to derive an instance of
Unboxable with Generic