Recently, I came up with the idea that one could potentially emulate "intersection types" in Haskell. Specifically, I mean intersection of "interfaces", as they are usually conceived in OOP languages. For instance, to use some pseudo-code for a language with interfaces and intersection types to see what I mean:
interface HasName {
val name: String
}
interface HasAge {
val age: Int
}
-- An object of type HasName & HasAge can call the methods of both
type HasNameAndAge = HasName & HasAge
-- For example:
val myObject: HasNameAndAge = ...
val example = "My name is: ${myObject.name}. My age is ${myObject.age}"
I am looking to do something similar with Haskell typeclasses. My approach was to replace interfaces with elements of kind * -> Constraint
in Haskell (so, for instance, single parameter type classes):
class HasName a where
name :: a -> String
class HasAge a where
age :: a -> Int
Now, given such type classes, the idea is that elements of types of the form exists a. C a => a
(where c :: * -> Constraint
) correspond to implementations of the "interface" C
. Given such an identification, we can easily construct non-anonymous intersection types by appending constraints, for instance:
{-# LANGUAGE ExistentialQuantification, RankNTypes #-}
-- The intersection of HasName and HasAge:
data HasNameAndAge = forall a. HasNameAndAge ((HasName a, HasAge a) => a)
-- Example:
data Person = Person {
personName :: String,
personAge :: Int
}
instance HasName Person where
name = personName
instance HasAge Person where
age = personAge
myObject :: HasNameAndAge
myObject = HasNameAndAge (Person "Nathan" 27)
The issue is, trying to generalize this and make this generic in a list [* -> Constraint]
of interfaces that an "object" implements, I am having trouble getting GHC to deduce what it needs to get this to work properly. This is my latest attempt:
{-# LANGUAGE
ConstraintKinds,
KindSignatures,
ExistentialQuantification,
DataKinds,
TypeFamilies,
TypeOperators,
RankNTypes,
AllowAmbiguousTypes,
TypeSynonymInstances,
FlexibleInstances,
MultiParamTypeClasses,
FlexibleContexts,
UndecidableInstances
#-}
import Data.Kind
class Elem (x :: * -> Constraint) (xs :: [* -> Constraint]) where
instance Elem x (x ': xs) where
instance Elem x xs => Elem x (y ': xs) where
type family All (cs :: [* -> Constraint]) a :: Constraint where
All '[] x = ()
All (c ': cs) x = (c x, All cs x)
-- The type of "objects" implementing all interfaces cs.
data AbstractType (cs :: [* -> Constraint])
= forall a. All cs a => AbstractType a
-- An example object of type HasName & HasAge.
myObject :: AbstractType '[HasName, HasAge]
myObject = AbstractType $ Person "Nathan" 27
-- Instances needed for every "interface" to get this to work.
instance Elem HasName cs => HasString (AbstractType cs) where
name (AbstractType x) = name x
instance Elem HasAge cs => HasAge (AbstractType cs) where
age (AbstractType x) = age x
-- Example:
myObject :: AbstractType '[HasName, HasAge]
myObject = AbstractType $ Person "Nathan" 27
example = do
print $ age myObject
putStrLn $ name myObject
It seems that I need to do a little more prodding to make GHC accept the instances I want at the end here. When I try compiling the above, I get errors like:
* Could not deduce (HasName a) arising from a use of `name'
from the context: Elem HasName cs
Intuitively, HasName
should hold for AbstractType cs
whenever HasName
is in cs
, since AbstractType cs
is an existential type with the constraint All cs a
. For instance, All '[HasName, HasAge] a = (HasName a, HasAge a)
, however, I am unsure of how to convince the GHC typechecker of this fact.
I am also getting errors like:
* No instance for (Elem HasName '[HasName, HasAge])
arising from a use of `name'
So it appears that either my implementation of a type-level elem
is incorrect, or GHC just can't test equality between terms of kind * -> Constraint
, so I am not sure if what I am trying to do is even possible with current versions of GHC.
Depends on what you want to do with that list, but one solution is to just flatten it away, and instead of using
Elem
to look up the constraint as a tag, use evidence that the desired constraint is implied by the intersection.To do that, you’d make
All
take a[Type -> Constraint]
and return aType -> Constraint
, so that you can partially apply it:Then store the dict for that conjunction of constraints in your existential type:
And finally, define instances for
Some
using a quantified constraint, namely, “Some c
is in the class of types that { have a name, have an age, … } ifc a
implies thata
is also in that class, for all typesa
”. In other words,c
must be a subset ofHasName
.Then your example works directly:
If you need to do more complex manipulations of the instances, and keep information about each constraint separately, then you can use the constraints package to explicitly store a dictionary value
Dict (c a)
, or multiple {Dict (c1 a)
, …,Dict (cn a)
}, instead of just the constraintc a => …
. An implication constraint can then be reified as a valuec a :- HasName a
, which you can obtain from an instance ofClass
(for superclass constraints) or:=>
(for instance constraints) depending on what you need it for.