Here you can use the usual heterogeneous list:
{-# LANGUAGE UndecidableInstances, GADTs, TypeFamilies, MultiParamTypeClasses, FunctionalDependencies, DataKinds, TypeOperators, FlexibleInstances #-} import Control.Applicative data HList xs where Nil :: HList '[] (:>) :: x -> HList xs -> HList (x ': xs) infixr 5 :> -- A Show instance, for illustrative purposes here. instance Show (HList '[]) where show _ = "Nil" instance (Show x, Show (HList xs)) => Show (HList (x ': xs)) where show (x :> xs) = show x ++ " : " ++ show xs
We usually write functions to HLists using classes, with one instance for Nil and another for the case :> . However, it would be nice to have a class for only one use case (namely, for Cartesian products here), so let me generalize the problem to an applicative sequence:
class Applicative f => HSequence f (xs :: [*]) (ys :: [*]) | xs -> ys, ys f -> xs where hsequence :: HList xs -> f (HList ys) instance Applicative f => HSequence f '[] '[] where hsequence = pure instance (Applicative g, HSequence f xs ys, y ~ x, f ~ g) => HSequence g (fx ': xs) (y ': ys) where hsequence (fx :> fxs) = (:>) <$> fx <*> hsequence fxs
Note the use of ~ restrictions in instance definition. This greatly facilitates type inference (along with functional dependencies in the class declaration); the general idea is to move as much information as possible from the instance head to the constraints, since this allows the GHC delay to solve them until there is enough contextual information.
Now Cartesian products work out of the box:
> hsequence ([1, 2] :> "ab" :> Nil) [1 : 'a' : Nil,1 : 'b' : Nil,2 : 'a' : Nil,2 : 'b' : Nil]
And we can also use hsequence with any Applicative :
> hsequence (Just "foo" :> Just () :> Just 10 :> Nil) Just "foo" : () : 10 : Nil
EDIT: I found out (thanks to dfeuer) that the same functionality is available from the existing hlist package:
import Data.HList.CommonMain > hSequence ([3, 4] .*. "abc" .*. HNil) [H[3, 'a'],H[3, 'b'],H[3, 'c'],H[4, 'a'],H[4, 'b'],H[4, 'c']]