By the time your Haskell is translated into GHC Core, the classes (and the attendant logic programming constructs such as impliciation) are nowhere to be seen. They are transformed by the compiler into dictionary passing code — each instance
becomes a record (a regular value) and each method becomes a member of that record. (For more specifics, see a previous answer of mine.)
So a constructor which packages up a constraint,
data Obj c where -- I'm using GADT syntax
Obj :: c a => a -> Obj c
is really represented at runtime by a regular product type,
data Obj c where
Obj :: c a -> a -> Obj c
where the c a
field is the runtime method dictionary representing the c a
instance.
To downcast an Obj c
to an Obj c'
at runtime, even if you had a way to test that the concrete a
was an instance of both c
and c'
, you'd still have to somehow synthesise a dictionary for c'
. Since c'
will generally contain more methods than c
, this amounts to asking the computer to write a program for you.
As David mentioned in the comments, I think your best bet would be to build knowledge about your specific class hierarchy into your runtime system under a closed-world assumption. If you have an oracle which can look up an instance's actual runtime dictionary,
oracle :: MonadRuntime m => TypeRep a -> TypeRep c -> m (Maybe (Dict (c a)))
then you can write cast
(with some uncomfortable type wrangling):
data Obj c where
Obj :: c a => TypeRep a -> a -> Obj c
cast :: forall c c' m. (MonadRuntime m, Typeable c') => Obj c -> m (Maybe (Obj c'))
cast (Obj tr x) = do
mdict <- oracle tr (typeRep @c')
case mdict of
Just Dict -> return (Just (Obj tr x))
Nothing -> return Nothing
Note that this cast
actually lets you (attempt to) change your object's interface to any other interface, not just those which are derived from the object's static type. (In C# you can do this by upcasting to object
and then downcasting.) You can prevent this by requiring an entailment in cast
's context:
cast :: forall c c' m. (MonadRuntime m, Typeable c', Class c c') => Obj c -> m (Maybe (Obj c'))
(Of course, that entailment would not actually be used at runtime.)
The challenge is to implement oracle
! I think it’s going to be one of those challenges which is not fun so I'll just give you a pointer or two.
Your Runtime
monad will probably be some sort of Reader
with a lookup table mapping (the TypeRep
s of) a
s and c
s to dictionaries. The a
s and c
s will need to be existentially quantified in order to store them in a heterogeneous list.
data TableEntry where
TableEntry :: c a => TypeRep c -> TypeRep a -> TableEntry
type MonadRuntime = MonadReader [TableEntry]
Then oracle
will need to look up the TableEntry
matching the class/type pair, then open the existential, establish the type equality by taking apart the typeRep
s, and return Just Dict
. (This part in particular sounds like a nightmare to code up.)
Before you run your MonadRuntime
program you'll need to construct the Table
containing all of the instances your program cares about.
table = [
TableEntry (typeRep @Ord) (typeRep @Int),
TableEntry (typeRep @Eq) (typeRep @Bool)
]
All in all I don’t see how it could possibly be worth the headache. Type classes are fundamentally unlike OO classes (and they're not even that much like OO interfaces), so it's really not surprising that it's hard to use them to model OO classes.