apen
apen

Reputation: 712

Haskell type synonym union?

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

Answers (2)

Jon Purdy
Jon Purdy

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

Enlico
Enlico

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

Related Questions