I’d like to share one scenario I have encountered using Haskell and a solution for it I have come up with. And I’d appreciate your feedback on my attempt.
I have modules that is defining a set of logic in “conceptual-level” where “irrelevant” details should be stripped off as much as possible, e.g. stringification(Show), serialization, etc.
Let’s look at one example:
class A a where
data AnyA = forall a. A a => MkAnyA a
data A1 = A1
instance A A1
data A2 = A2
instance A A2
someAs :: [AnyA]
someAs = [MkAnyA A1, MkAnyA A2, MkAnyA A1]
-- PROBLEM: How to extend "Show" instances to all A-s so that we can do "show a"
instance Show AnyA where show (MkAnyA a) = "Which A?"
main :: IO ()
main = do
(putStrLn . show) someAs
Here is the solution I have, the trick is to use a ExtraConstraints
open type family as a “extension point” for future integrations. Here is the full code:
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableSuperClasses #-}
import Data.Kind (Constraint, Type)
-- | Extra constraints type family served as type class extension mechanism for the type
type family ExtraConstraints (a :: Type -> Constraint) :: Type -> Constraint
--------------------------------------------------------------------------------
-- Concept-level
--
-- In this level, we want the definitions stripping off as much as irrelevant
-- details possible.
--------------------------------------------------------------------------------
-- * A as a conceptual-level type class needs not to know what integrations would be
-- added to the system-level instances of A.
-- * To allow future extensions, ExtraConstraints is provided as the unknown constraints
-- decided only in the system-level instances.
-- * GHC is paranoid about cyclic constraints, so it wants UndecidableSuperClasses.
class (ExtraConstraints A a) => A a where
-- * E.g. an existential type of all A-s needs not to know the actual constraints in the system-level
data AnyA = forall a. A a => MkAnyA a
-- * Let's define some instances
data A1 = A1
instance A A1
data A2 = A2
instance A A2
-- * Let's define a function that uses these instances
someAs :: [AnyA]
someAs = [MkAnyA A1, MkAnyA A2, MkAnyA A1]
--------------------------------------------------------------------------------
-- System-level
--
-- In this level, we apply the concepts and add integration to them.
--------------------------------------------------------------------------------
-- * E.g. We integrate the "Show" type class for all A-s
type instance ExtraConstraints A = Show
-- * Show instances need to be defined, otherwise GHC would throw "No insance" errors
instance Show A1 where show _ = "A1"
instance Show A2 where show _ = "A2"
-- * Now the system can comfortably have a Show instance for the AnyA
-- * Note that A did not need to know Show was required constraints in the system-level
instance Show AnyA where show (MkAnyA a) = show a
main :: IO ()
main = do
(putStrLn . show) someAs
Cheers,