Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

type FruitSalad = { 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

Slide 3

Slide 3 text

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 This can be read as: • 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.

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

data FruitSalad = FruitSalad { 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 case class FruitSalad( 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 record FruitSalad( 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}

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

data FruitSalad = FruitSalad { 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 salad = FruitSalad GoldenDelicious Cavendish Montmorency sameSalad = FruitSalad GoldenDelicious Cavendish Montmorency differentSalad = FruitSalad GoldenDelicious Manzano Montmorency snack = Apple GoldenDelicious sameSnack = Apple GoldenDelicious differentSnack = Banana Cavendish in do assert (show salad == "FruitSalad {apple = GoldenDelicious, banana = Cavendish, cherries = Montmorency}") pure () assert (salad == sameSalad) return () assert (salad /= differentSalad) return () 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’ assert(snack /= salad) return () -- 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’ assert(salad /= GoldenDelicious) return () 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).

Slide 8

Slide 8 text

val salad = FruitSalad(GoldenDelicious, Cavendish, Montmorency); val sameSalad = FruitSalad(GoldenDelicious, Cavendish, Montmorency); val differentSalad = FruitSalad(GoldenDelicious, Manzano, Montmorency); val snack = Apple(GoldenDelicious) val sameSnack = Apple(GoldenDelicious) val differentSnack = Banana(Cavendish) assert(salad.toString == "FruitSalad(GoldenDelicious,Cavendish,Montmorency)") assert(salad == sameSalad) assert(salad != differentSalad) assert(snack.toString == "Apple(GoldenDelicious)") assert(snack == sameSnack); assert(snack != differentSnack); // Compiler error: Values of types FruitSalad and FruitSnack cannot be compared with == or != assert(salad != snack) // Compiler error: Values of types FruitSalad and AppleVariety cannot be compared with == or != assert(salad != GoldenDelicious) // Compiler error: Values of types FruitSnack and AppleVariety cannot be compared with == or != assert(snack != GoldenDelicious) case class FruitSalad( 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 added the following to FruitSalad and FruitSnack: derives CanEqual.

Slide 9

Slide 9 text

case class FruitSalad( 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)

Slide 10

Slide 10 text

record FruitSalad( 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 salad = new FruitSalad(GOLDEN_DELICIOUS,CAVENDISH, MONTMORENCY); var sameSalad = new FruitSalad(GOLDEN_DELICIOUS,CAVENDISH, MONTMORENCY); var differentSalad = new FruitSalad(GOLDEN_DELICIOUS,MANZANO, MONTMORENCY); var snack = new Apple(GOLDEN_DELICIOUS); var sameSnack = new Apple(GOLDEN_DELICIOUS); var differentSnack = new Banana(CAVENDISH); assert(salad.toString().equals("FruitSalad[apple=GOLDEN_DELICIOUS, banana=CAVENDISH, cherries=MONTMORENCY]")); assert(salad.equals(sameSalad)); assert(!salad.equals(differentSalad)); assert(snack.toString().equals("Apple[variety=GOLDEN_DELICIOUS]")); assert(snack.equals(sameSnack)); assert(!snack.equals(differentSnack)); assert(!salad.equals(snack)); assert(!salad.equals(GOLDEN_DELICIOUS)); assert(!snack.equals(GOLDEN_DELICIOUS));

Slide 11

Slide 11 text

Now, let’s see some pattern matching @philip_schwarz

Slide 12

Slide 12 text

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." data FruitSalad = FruitSalad { 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."

Slide 13

Slide 13 text

case class FruitSalad( 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 val pickyCustomerReaction = salad match 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."

Slide 14

Slide 14 text

record FruitSalad( 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} String pickyCustomerReaction(FruitSalad salad) { return switch (salad) { 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."; }; }

Slide 15

Slide 15 text

In order to run that pattern matching code, I downloaded the Java 19 early access build.

Slide 16

Slide 16 text

$ ~/Downloads/jdk-19.jdk/Contents/Home/bin/jshell --enable-preview | Welcome to JShell -- Version 19-ea | For an introduction type: /help intro jshell> record FruitSalad( ...> 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>

Slide 17

Slide 17 text

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>

Slide 18

Slide 18 text

jshell> String pickyCustomerReaction(FruitSalad salad) { ...> return switch (salad) { ...> 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."; ...> }; ...> } | created method pickyCustomerReaction(FruitSalad) jshell> var salad = new FruitSalad(AppleVariety.GOLDEN_DELICIOUS,BananaVariety.CAVENDISH, CherryVariety.MONTMORENCY); salad ==> FruitSalad[apple=GOLDEN_DELICIOUS, banana=CAVENDISH, cherries=MONTMORENCY] jshell> pickyCustomerReaction(salad); $14 ==> "I can't stand Golden Delicious apples." jshell>

Slide 19

Slide 19 text

I had a go at applying those suggestions. The first one was fine. When I tried the second one, I ran into some issues. If I find out more from Brian Goetz, or I manage to resolve the issue, then I’ll publish a new version of this deck.

Slide 20

Slide 20 text

That’s all. I hope you found it useful. @philip_schwarz