Seeing and Solving the Problem in FP Code
The goal is to define a data type by cases, where
- one can add new cases to the data type and
- (one can add) new functions over the data type,
- without recompiling existing code, and
- while retaining static type safety.
A Very Simple Example of The Problem
Given this code
data Fruit
= Apple
| Banana
showFruit :: Fruit -> String
showFruit Apple = "apple"
showFruit Banana = "banana"
We can easily add a new function to our code without needing to recompile our existing code
-- in another file...
intFruit :: Fruit -> Int
intFruit Apple = 0
intFruit Banana = 1
However, if we want to add another data constructor to Fruit
, we can only do so by updating Fruit
to include Orange
and then updating all of our functions to include Orange
as well:
data Fruit
= Apple
| Banana
| Orange
showFruit :: Fruit -> String
showFruit Apple = "apple"
showFruit Banana = "banana"
showFruit Orange = "orange"
Since Fruit
has already been compiled, we will need to recompile our code with the updated version of Fruit
. Moreover, if we do not update showFruit
/intFruit
, then we no longer have an exhaustive pattern match. Thus, these functions are no longer pure but are now partial functions.
The Solution
The solution, then, is to be able to define data types in such a way that they "compose". The best way to compose data types is to group two types into one type via a type wrapper:
-- original file
data Fruit
= Apple
| Banana
-- new file
data Fruit2
= Orange
data FruitGrouper = -- ???
How should FruitGrouper
be defined? A value of FruitGrouper
should only be one of 3 values:
- FruitGrouper Apple
- FruitGrouper Banana
- FruitGrouper Orange
We can define it using this approach:
data FruitGrouper
= Fruit_ Fruit
| Fruit2_ Fruit2
This approach will enable showFruit
and intFruit
to continue to work as expected. If we wanted to define a new function that uses both, we would pass in FruitGrouper
instead:
-- original file. This cannot change once written!
data Fruit
= Apple
| Banana
showFruit :: Fruit -> String
showFruit Apple = "apple"
showFruit Banana = "banana"
-- new file
data Fruit2
= Orange
data FruitGrouper
= Fruit_ Fruit
| Fruit2_ Fruit2
showAllFruit :: FruitGrouper -> String
showAllFruit (Fruit_ appleOrBanana) = showFruit appleOrBanana
showAllFruit (Fruit2_ Orange) = "orange"
Great! We have now seen how to solve a very simple version of this problem. Now, let's refine this approach a bit as preparation for a future harder problem.