Search code examples
haskelldata-structurestypinglevels

How to have multiple types under a generic "super type" in Haskell?


[Never mind this question - In light of the feedback, I realize it is poorly phrased and too vague. I do not delete it because the answer could serve others. Thanks!]

I am looking for a type structure that allows me to put two similar ordered data types under one generic type.

For example, let's assume I have the following ordered types:

data Type1 = Type1Level1 | Type1Level2 | Type1Level3 | Type1Level4 deriving (Eq, Ord, Show, Read, Bounded, Enum) 

data Type2 = Type2Level1 | Type2Level2 | Type2Level3 deriving (Eq, Ord, Show, Read, Bounded, Enum) 

In the spirit, I would like a generic overarching type like:

data GenType = Type1 | Type2

But of course that won't work.

I happen to have entities that can be related to one of those types:

data Entity = Entity {
  entityId :: String,
  entityType :: -- here I want one of the TypeXLevelY (anything "under GenType")
 } deriving (Eq, Ord, Read, Show)

But I'd also like those entities to have specific fields depending if they are of type1 or type2 (does this mean I have to make 2 different "entity" data types?). For example, an entity of type1 should have a field "input" but not the entity of type2.

Also, type1 and type2 should be able to share ordering and several functions in common, such that something like this would make sense:

testSuccType :: GenType -> GenType -> Bool
testSuccType type1 type2 = (type2 == succ type1)

It looks like I'm needing a class here but (1) I'm not sure that's the case and (2) when I tried to use qualified type for Entity, it didn't like it (apparently this kind of qualification is not supported anymore):

data GenType a => Entity = Entity {
  entityId :: String,
  entityType :: a
} deriving (Eq, Ord, Read, Show)

I hope this is clear enough. Many thanks in advance for your help.


Solution

  • I guess I don't fully understand the question, but perhaps I can give some hints anyway.

    Probably the data type definition you are looking for is:

    data GenType = GT1 Type1 | GT2 Type2
    

    Or, somewhat idiomatically, you could pun the constructor names with the type of their (only) field:

    data GenType = Type1 Type1 | Type2 Type2
    

    Then you can implement your shared function like this:

    getNextLevelGenType (GT1 t1) = GT1 (getNextLevelType1 t1)
    getNextLevelGenType (GT2 t2) = GT2 (getNextLevelType2 t2)
    
    -- OR
    
    getNextLevelGenType (Type1 t1) = Type1 (getNextLevelType1 t1)
    getNextLevelGenType (Type2 t2) = Type2 (getNextLevelType2 t2)
    

    Sometimes it makes sense to share method names via a type class, but whether this makes sense depends a lot on details not shared in the question. If it did make sense, it might look like this:

    class Leveled a where getNextLevel :: a -> a
    instance Leveled Type1 where getNextLevel = succ
    instance Leveled Type2 where getNextLevel = succ
    instance Leveled GenType where
        getNextLevel (Type1 t1) = Type1 (getNextLevel t1)
        getNextLevel (Type2 t2) = Type2 (getNextLevel t2)
    

    If you go that route, there is a generic, parameterized sum type that may be worth considering as an alternative to GenType, named Either. The main reason to avoid considering it is if GenType's constructor names can serve as human-readable documentation of their meaning. (The examples above, where the names indicate the field type and nothing more, are not good human-readable documentation.) A secondary reason would be if you actually want a sum of three or more types, since nested Eithers get unwieldy.

    instance (Leveled a, Leveled b) => Leveled (Either a b) where
        getNextLevel (Left a) = Left (getNextLevel a)
        getNextLevel (Right b) = Right (getNextLevel b)
    
        -- OR, with Data.Bifunctor imported,
    
        getNextLevel = bimap getNextLevel getNextLevel