data Fruit = Apple | Orange | ...
data Meat = Chicken | Beef | ...
You can’t construct an untagged union directly. However, you can construct something that is equivalent to a tagged union (sum type) but that doesn’t require tags, using functions instead.
A sum type like Either a b can be represented by its pattern-matching function like either :: (a -> c) -> (b -> c) -> Either a b -> c; the expression either f g e is equivalent to the case expression case e of { Left x -> f x; Right y -> g y }.
So whenever you would use data Food = Fruit | Meat | Vegetable | …, you could instead use the function equivalent to matching on this type. That is, converting from this:
data Food
= Fruit Fruit
| Meat Meat
| Vegetable Vegetable
-- …
-- Energy density in kilojoules per gram
newtype EnergyDensity = ED Word
foodED :: Food -> EnergyDensity
foodED (Fruit f) = fruitED f
foodED (Meat m) = meatED m
foodED (Vegetable v) = vegetableED v
-- …
To this:
{-# Language RankNTypes #-}
-- A food-matching function.
type FoodF r
= (Fruit -> r)
-> (Meat -> r)
-> (Vegetable -> r)
-- …
-> r
-- A polymorphic matching function.
newtype Food = Food { matchFood :: forall r. FoodF r }
foodED :: Food -> EnergyDensity
foodED food = matchFood food
(\ f -> fruitED f)
(\ m -> meatED m)
(\ v -> vegetableED v)
-- …
-- Or, simplified:
--
-- f food = matchFood food fruitED meatED vegetableED
To construct this type, you simply make a Food which calls the appropriate case:
{-# Language BlockArguments #-}
fruit :: Fruit -> Food
fruit x = Food \ f _m _v -> f x
meat :: Meat -> Food
meat x = Food \ _f m _v -> m x
vegetable :: Vegetable -> Food
vegetable x = Food \ _f _m v -> v x
Since “positional” matching like this can be unwieldy, you could also introduce a record type for matching. However, this likely isn’t as “clean” or straightforward as you had in mind.
The basic problem is that we’re making a hierarchy of types and then trying to remove the resulting nested tags. It’s often preferable to avoid the nesting to begin with by making a single type of “foods”, where things like “is fruit” are properties of the food. This can be done dynamically, by defining functions:
import Data.Set (Set)
import qualified Data.Set as Set
data Food
= Apple
| Orange
-- …
| Chicken
| Beef
-- …
isFruit :: Food -> Bool
isFruit Apple = True
isFruit Orange = True
-- …
isFruit Chicken = False
isFruit Beef = False
-- …
data Animal = Fowl | Beast | Fish
foodAnimal :: Food -> Maybe Animal
foodAnimal Chicken = Just Fowl
foodAnimal Beef = Just Beast
foodAnimal _ = Nothing
animalProducts :: [Food] -> Set Animal
animalProducts foods
= Set.fromList
[ animal
| food <- foods
, Just animal <- foodAnimal food
]
Or statically, if all of the categories are disjoint, using a type tag on the data type:
{-# Language
DataKinds,
GADTs,
KindSignatures #-}
data Food (t :: FoodGroup) where
Apple :: Food 'FruitGroup
Orange :: Food 'FruitGroup
-- …
Chicken :: Food 'MeatGroup
Beef :: Food 'MeatGroup
-- …
data FoodGroup
= FruitGroup
| MeatGroup
| VegetableGroup
-- …
-- Aliases for subsets of ‘Food’.
type Fruit = Food 'FruitGroup
type Meat = Food 'MeatGroup
type Vegetable = Food 'VegetableGroup
-- Non-meat cases are impossible because t=MeatGroup, so
-- we don’t need to match them, and don’t need to return
-- a ‘Maybe’-wrapped result.
meatAnimal :: Food 'MeatGroup -> Animal
meatAnimal Chicken = Fowl
meatAnimal Beef = Beast
This can give you more type safety and convenience of pattern matching, but it also requires you to be careful about the design of your categories: where will you put Tomato?