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
Elemto look up the constraint as a tag, use evidence that the desired constraint is implied by the intersection.To do that, you’d make
Alltake 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
Someusing a quantified constraint, namely, “Some cis in the class of types that { have a name, have an age, … } ifc aimplies thatais also in that class, for all typesa”. In other words,cmust 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.