Reputation: 712
In Haskell, I want to do something like the following
data Fruits = Apple | Orange | ...
data Meat = Chicken | Beef | ...
type Eats = Fruits | Meat
I wish to construct type Eats
such that it is a union of two types. The point is that I want to do this without adding another layer of constructors. Is this possible in Haskell at all?
Upvotes: 6
Views: 1925
Reputation: 54971
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
?
Upvotes: 4
Reputation: 28406
From the wiki you can read this
type
introduces a synonym for a type and uses the same data constructors.
If it introduces a synonym, then how can one word (Eats
) be synonym of both other two words (Fruits
and Meat
) without causing ambiguity?
Considered that Fruits
and Meat
are types, so they are expected where types are used, for instance in signatures. Now how would the compiler go about the following?
myfun :: Eats -> Int
-- some definition of myfun
Upvotes: 2