$30 off During Our Annual Pro Sale. View Details »

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
PRO

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

    View Slide

  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

    View Slide

  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.

    View Slide

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

    View Slide

  5. 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}

    View Slide

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

    View Slide

  7. 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).

    View Slide

  8. 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.

    View Slide

  9. 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)

    View Slide

  10. 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));

    View Slide

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

    View Slide

  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."
    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."

    View Slide

  13. 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."

    View Slide

  14. 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.";
    };
    }

    View Slide

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

    View Slide

  16. $ ~/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>

    View Slide

  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>

    View Slide

  18. 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>

    View Slide

  19. 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.

    View Slide

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

    View Slide