Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

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.

Download for better results.

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

Philip Schwarz

July 03, 2022
Tweet

More Decks by Philip Schwarz

Other Decks in Programming

Transcript

  1. Sum and Product Types The Fruit Salad & Fruit Snack

    Example From F# to Haskell, Scala and Java @ScottWlaschin
  2. 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
  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 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.
  4. 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}
  5. Now, let’s see what the behaviour is when we compare

    sample values and when we convert them to strings.
  6. 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).
  7. 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.
  8. 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)
  9. 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));
  10. 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."
  11. 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."
  12. 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."; }; }
  13. $ ~/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>
  14. 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>
  15. 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>
  16. 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.