Philip Schwarz
PRO
July 03, 2022
270

# Sum and Product Types - The Fruit Salad & Fruit Snack Example - From F# to Haskell, Scala and Java

Sum and Product Types -The Fruit Salad & Fruit Snack Example - From F# to Haskell, Scala and Java.

Inspired by the example in Scott Wlaschin’s F# book: Domain Modeling Made Functional.

Keywords: algebraic data type, and, data oriented programming, f#, functional programming, haskell, java, java 19, or, product type, scala, scott wlaschin, sum type

July 03, 2022

## Transcript

1. Sum and Product Types
The Fruit Salad & Fruit Snack Example
From F# to Haskell, Scala and Java
@ScottWlaschin

Apple: AppleVariety
Banana: BananaVariety
Cherries: CherryVariety
}
In F#, new types are built from smaller types in two ways:
• By _AND_ing them together
• By _OR_ing them together
“AND” Types
Let’s start with building types using AND. For example, we might say that to
make fruit salad you need an apple and a banana and some cherries:
“OR” Types
The other way of building new types is by using OR. For example, we might
say that for a fruit snack you need an apple or a banana or some cherries:
@ScottWlaschin
type FruitSnack =
| Apple of AppleVariety
| Banana of BananaVariety
| Cherries of CherryVariety

3. The varieties of fruit are themselves defined as OR types, which in this case is used
similarly to an enum in other languages.
type AppleVariety =
| GoldenDelicious
| GrannySmith
| Fuji
type BananaVariety =
| Cavendish
| GrosMichel
| Manzano
type CherryVariety =
| Montmorency
| Bing
• An AppleVariety is either a GoldenDelicious or a GrannySmith or a Fuji, and so on.
@ScottWlaschin
Jargon Alert: “Product Types” and “Sum Types”
The types that are built using AND are called product types.
The types that are built using OR are called sum types or tagged unions or, in F#
terminology, discriminated unions. In this book I will often call them choice types,
because I think that best describes their role in domain modeling.

4. @philip_schwarz
Let’s translate that F# example into Haskell, Scala and Java.

apple :: AppleVariety,
banana :: BananaVariety,
cherries :: CherryVariety
}
data FruitSnack
= Apple AppleVariety
| Banana BananaVariety
| Cherries CherryVariety
data AppleVariety
= GoldenDelicious
| GrannySmith
| Fuji
data BananaVariety
= Cavendish
| GrosMichel
| Manzano
data CherryVariety
= Montmorency
| Bing
apple: AppleVariety,
banana: BananaVariety,
cherries: CherryVariety
)
enum FruitSnack:
case Apple(variety: AppleVariety)
case Banana(variety: BananaVariety)
case Cherries(variety: CherryVariety)
enum AppleVariety:
case GoldenDelicious,
GrannySmith,
Fuji
enum BananaVariety:
case Cavendish,
GrosMichel,
Manzano
enum CherryVariety:
case Montmorency,
Bing
AppleVariety apple,
BananaVariety banana,
CherryVariety cherries
) { }
sealed interface FruitSnack permits Apple, Banana, Cherries { }
record Apple(AppleVariety variety) implements FruitSnack { }
record Banana(BananaVariety variety) implements FruitSnack { }
record Cherries(CherryVariety variety) implements FruitSnack { }
enum AppleVariety {
GOLDEN_DELICIOUS,
GRANNY_SMITH,
FUJI}
enum BananaVariety {
CAVENDISH,
GROS_MICHEL,
MANZANO}
enum CherryVariety {
MONTMORENCY,
BING}

6. Now, let’s see what the behaviour is when we compare
sample values and when we convert them to strings.

apple :: AppleVariety,
banana :: BananaVariety,
cherries :: CherryVariety
} deriving (Eq, Show)
data FruitSnack
= Apple AppleVariety
| Banana BananaVariety
| Cherries CherryVariety
deriving (Eq, Show)
data AppleVariety
= GoldenDelicious
| GrannySmith
| Fuji
deriving (Eq, Show)
data BananaVariety
= Cavendish
| GrosMichel
| Manzano
deriving (Eq, Show)
data CherryVariety
= Montmorency
| Bing
deriving (Eq, Show)
main :: IO ()
main =
let
snack = Apple GoldenDelicious
sameSnack = Apple GoldenDelicious
differentSnack = Banana Cavendish
in do
assert (show salad == "FruitSalad {apple = GoldenDelicious, banana = Cavendish, cherries = Montmorency}") pure ()
assert (show snack == "Apple GoldenDelicious") return ()
assert (snack == sameSnack) return ()
assert (snack /= differentSnack) return ()
-- Error: Couldn't match expected type ‘FruitSalad’ with actual type ‘FruitSnack’
-- Error: Couldn't match expected type ‘FruitSnack’ with actual type ‘AppleVariety’
assert(snack /= GoldenDelicious) return ()
-- Error: Couldn't match expected type ‘FruitSnack’ with actual type ‘AppleVariety’
To permit the ‘showing’ of FruitSalad and
FruitSnack values, and also to permit the
comparison of such values, we have added the
following to all types: deriving (Eq, Show).

val snack = Apple(GoldenDelicious)
val sameSnack = Apple(GoldenDelicious)
val differentSnack = Banana(Cavendish)
assert(snack.toString == "Apple(GoldenDelicious)")
assert(snack == sameSnack);
assert(snack != differentSnack);
// Compiler error: Values of types FruitSalad and FruitSnack cannot be compared with == or !=
// Compiler error: Values of types FruitSalad and AppleVariety cannot be compared with == or !=
// Compiler error: Values of types FruitSnack and AppleVariety cannot be compared with == or !=
assert(snack != GoldenDelicious)
apple: AppleVariety,
banana: BananaVariety,
cherries: CherryVariety
) derives CanEqual
enum FruitSnack derives CanEqual:
case Apple(variety: AppleVariety)
case Banana(variety: BananaVariety)
case Cherries(variety: CherryVariety)
enum AppleVariety:
case GoldenDelicious,
GrannySmith,
Fuji
enum BananaVariety:
case Cavendish,
GrosMichel,
Manzano
enum CherryVariety:
case Montmorency,
Bing
To prevent meaningless comparisons, e.g.
comparing a salad with a snack, we have
FruitSnack: derives CanEqual.

apple: AppleVariety,
banana: BananaVariety,
cherries: CherryVariety
) derives CanEqual
enum FruitSnack derives CanEqual:
case Apple(variety: AppleVariety)
case Banana(variety: BananaVariety)
case Cherries(variety: CherryVariety)
enum AppleVariety:
case GoldenDelicious,
GrannySmith,
Fuji
enum BananaVariety:
case Cavendish,
GrosMichel,
Manzano
enum CherryVariety:
case Montmorency,
Bing
products (AND)
degenerate products - single argument
product (AND)
sum (OR)

AppleVariety apple,
BananaVariety banana,
CherryVariety cherries
) { }
sealed interface FruitSnack permits Apple, Banana, Cherries { }
record Apple(AppleVariety variety) implements FruitSnack { }
record Banana(BananaVariety variety) implements FruitSnack { }
record Cherries(CherryVariety variety) implements FruitSnack { }
enum AppleVariety {
GOLDEN_DELICIOUS,
GRANNY_SMITH,
FUJI}
enum BananaVariety {
CAVENDISH,
GROS_MICHEL,
MANZANO}
enum CherryVariety {
MONTMORENCY,
BING}
var snack = new Apple(GOLDEN_DELICIOUS);
var sameSnack = new Apple(GOLDEN_DELICIOUS);
var differentSnack = new Banana(CAVENDISH);
assert(snack.toString().equals("Apple[variety=GOLDEN_DELICIOUS]"));
assert(snack.equals(sameSnack));
assert(!snack.equals(differentSnack));
assert(!snack.equals(GOLDEN_DELICIOUS));

11. Now, let’s see some pattern matching
@philip_schwarz

12. pickyCustomerReaction :: FruitSalad -> String
pickyCustomerReaction (FruitSalad Fuji Cavendish Bing) = "That's my favourite combination."
pickyCustomerReaction (FruitSalad GoldenDelicious _ _) = "I can't stand Golden Delicious apples."
pickyCustomerReaction (FruitSalad _ Manzano Bing) = "I both love and hate this."
pickyCustomerReaction (FruitSalad _ Manzano _) = "Manzano is my least favourite banana."
pickyCustomerReaction (FruitSalad _ _ Bing) = "Bing are my favourite cherries."
pickyCustomerReaction _ = "It will do."
apple :: AppleVariety,
banana :: BananaVariety,
cherries :: CherryVariety
} deriving (Eq, Show)
data FruitSnack
= Apple AppleVariety
| Banana BananaVariety
| Cherries CherryVariety
deriving (Eq, Show)
data AppleVariety
= GoldenDelicious
| GrannySmith
| Fuji
deriving (Eq, Show)
data BananaVariety
= Cavendish
| GrosMichel
| Manzano
deriving (Eq, Show)
data CherryVariety
= Montmorency
| Bing
deriving (Eq, Show)
pickySnackerRemark :: FruitSnack -> String
pickySnackerRemark (Apple Fuji) = "That's my favourite apple."
pickySnackerRemark (Apple GoldenDelicious) = "I can't stand Golden Delicious apples."
pickySnackerRemark (Banana Cavendish) = "That's my favourite banana."
pickySnackerRemark (Banana Manzano) = "Manzano is my least favourite banana."
pickySnackerRemark (Cherries Bing) = "Those are my favourite cherries."
pickySnackerRemark _ = "It will do."

apple: AppleVariety,
banana: BananaVariety,
cherries: CherryVariety
)
enum FruitSnack:
case Apple(variety: AppleVariety)
case Banana(variety: BananaVariety)
case Cherries(variety: CherryVariety)
enum AppleVariety:
case GoldenDelicious,
GrannySmith,
Fuji
enum BananaVariety:
case Cavendish,
GrosMichel,
Manzano
enum CherryVariety:
case Montmorency,
Bing
case FruitSalad(Fuji,Cavendish,Bing) => "That's my favourite combination."
case FruitSalad(GoldenDelicious,_,_) => "I can't stand Golden Delicious apples.”
case FruitSalad(_,Manzano,Bing) => "I both love and hate this."
case FruitSalad(_,Manzano,_) => "Manzano is my least favourite banana."
case FruitSalad(_,_,Bing) => "Bing are my favourite cherries."
case _ => "It will do."
val pickySnackerRemark = snack match
case Apple(Fuji) => "That's my favourite apple."
case Apple(GoldenDelicious) => "I can't stand Golden Delicious apples."
case Banana(Cavendish) => "That's my favourite banana."
case Banana(Manzano) => "Manzano is my least favourite banana."
case Cherries(Bing) => "Those are my favourite cherries."
case _ => "It will do."

AppleVariety apple,
BananaVariety banana,
CherryVariety cherries
) { }
sealed interface FruitSnack permits Apple, Banana, Cherries { }
record Apple(AppleVariety variety) implements FruitSnack { }
record Banana(BananaVariety variety) implements FruitSnack { }
record Cherries(CherryVariety variety) implements FruitSnack { }
enum AppleVariety {
GOLDEN_DELICIOUS,
GRANNY_SMITH,
FUJI}
enum BananaVariety {
CAVENDISH,
GROS_MICHEL,
MANZANO}
enum CherryVariety {
MONTMORENCY,
BING}
case FruitSalad(var apple, var banana, var cherries)
when apple.equals(FUJI) && banana.equals(CAVENDISH)
&& cherries.equals(BING) ->
"That's my favourite combination.";
case FruitSalad(var apple, var banana, var cherries)
when apple.equals(GOLDEN_DELICIOUS) ->
"I can't stand Golden Delicious apples.";
case FruitSalad(var apple, var banana, var cherries)
when banana.equals(MANZANO) && cherries.equals(BING) ->
"I both love and hate this.";
case FruitSalad(var apple, var banana, var cherries)
when banana.equals(MANZANO) ->
"Manzano is my least favourite banana.";
case FruitSalad(var apple, var banana, var cherries)
when cherries.equals(BING) ->
"Bing are my favourite cherries.";
default -> "It will do.";
};
}
String pickySnackerRemark(FruitSnack snack) {
return switch (snack) {
case Apple(var variety) when variety.equals(FUJI) ->"That's my favourite apple.";
case Apple(var variety) when variety.equals(GOLDEN_DELICIOUS) ->"I can't stand Golden Delicious apples.";
case Banana(var variety) when variety.equals(CAVENDISH) ->"That's my favourite banana.";
case Banana(var variety) when variety.equals(MANZANO) ->"Manzano is my least favourite banana.";
case Cherries(var variety) when variety.equals(BING) ->"Those are my favourite cherries.";
default -> "It will do.";
};
}

15. In order to run that pattern matching code, I

| Welcome to JShell -- Version 19-ea
| For an introduction type: /help intro
...> AppleVariety apple,
...> BananaVariety banana,
...> CherryVariety cherries
...> ) { }
...>
...> sealed interface FruitSnack permits Apple, Banana, Cherries { }
...> record Apple(AppleVariety variety) implements FruitSnack { }
...> record Banana(BananaVariety variety) implements FruitSnack { }
...> record Cherries(CherryVariety variety) implements FruitSnack { }
...>
...> enum AppleVariety {GOLDEN_DELICIOUS, GRANNY_SMITH, FUJI}
...> enum BananaVariety {CAVENDISH, GROS_MICHEL, MANZANO}
...> enum CherryVariety {MONTMORENCY, BING}
| created record FruitSalad, however, it cannot be referenced until class AppleVariety, class BananaVariety, and class CherryVariety are declared
| created interface FruitSnack, however, it cannot be referenced until class Apple, class Banana, and class Cherries are declared
| created record Apple, however, it cannot be referenced until class AppleVariety is declared
| created record Banana, however, it cannot be referenced until class BananaVariety is declared
| created record Cherries, however, it cannot be referenced until class CherryVariety is declared
| created enum AppleVariety
| created enum BananaVariety
| created enum CherryVariety
jshell>

17. jshell> String pickySnackerRemark(FruitSnack snack) {
...> return switch (snack) {
...> case Apple(var variety) when variety.equals(AppleVariety.FUJI) ->"That's my favourite apple.";
...> case Apple(var variety) when variety.equals(AppleVariety.GOLDEN_DELICIOUS) ->"I can't stand Golden Delicious apples.";
...> case Banana(var variety) when variety.equals(BananaVariety.CAVENDISH) ->"That's my favourite banana.";
...> case Banana(var variety) when variety.equals(BananaVariety.MANZANO) ->"Manzano is my least favourite banana.";
...> case Cherries(var variety) when variety.equals(CherryVariety.BING) ->"Those are my favourite cherries.";
...> default -> "It will do.";
...> };
...> }
| created method pickySnackerRemark(FruitSnack)
jshell> FruitSnack snack = new Banana(BananaVariety.MANZANO);
snack ==> Banana[variety=MANZANO]
jshell> pickySnackerRemark(snack)
\$11 ==> "Manzano is my least favourite banana.”
jshell>

...> case FruitSalad(var apple, var banana , var cherries)
...> when apple.equals(AppleVariety.FUJI) && banana.equals(BananaVariety.CAVENDISH) && cherries.equals(CherryVariety.BING) ->
...> "That's my favourite combination.";
...> case FruitSalad(var apple, var banana , var cherries)
...> when apple.equals(AppleVariety.GOLDEN_DELICIOUS) ->
...> "I can't stand Golden Delicious apples.";
...> case FruitSalad(var apple, var banana , var cherries)
...> when banana.equals(BananaVariety.MANZANO) ->
...> "Manzano is my least favourite banana.";
...> case FruitSalad(var apple, var banana , var cherries)
...> when cherries.equals(CherryVariety.BING) ->
...> "Bing are my favourite cherries.";
...> case FruitSalad(var apple, var banana , var cherries)
...> when banana.equals(BananaVariety.MANZANO) && cherries.equals(CherryVariety.BING) ->
...> "I both love and hate this.";
...> default -> "It will do.";
...> };
...> }