Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save mattbarackman/82b1712add45ceffa8ffe56f81b8bcc2 to your computer and use it in GitHub Desktop.
Save mattbarackman/82b1712add45ceffa8ffe56f81b8bcc2 to your computer and use it in GitHub Desktop.

Revisions

  1. mattbarackman revised this gist Jul 20, 2016. 1 changed file with 2 additions and 0 deletions.
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,7 @@
    # Scala: Type Families, Sealed Traits, and Exhaustive Pattern Matching

    By: Matt Barackman

    ## What is a Type Family?

    A collection of **objects** or **case classes** that share a **sealed trait**.
  2. mattbarackman revised this gist Jun 24, 2016. 1 changed file with 1 addition and 1 deletion.
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,4 @@
    # Type Families, Sealed Traits, and Exhaustive Pattern Matching
    # Scala: Type Families, Sealed Traits, and Exhaustive Pattern Matching

    ## What is a Type Family?

  3. mattbarackman revised this gist Jun 3, 2016. 1 changed file with 1 addition and 1 deletion.
    Original file line number Diff line number Diff line change
    @@ -199,7 +199,7 @@ So, using sealed traits can help ensure that as you extend your type family you

    ## Final Thoughts

    When you find yourself with a collection of similar objects or case classes that you want to treat as one type in some scenarios, but as individual members in others (particularly when you will be pattern matching) lean towards using sealed traits / type families.
    When you find yourself with a collection of similar objects or case classes that you want to treat as one type in some scenarios, but as individual members in others (particularly when you will be pattern matching) lean towards using sealed traits and type families.

    It will:

  4. mattbarackman revised this gist Jun 3, 2016. 1 changed file with 1 addition and 1 deletion.
    Original file line number Diff line number Diff line change
    @@ -199,7 +199,7 @@ So, using sealed traits can help ensure that as you extend your type family you

    ## Final Thoughts

    When you find yourself with a collection of similar objects or case classes that you want to treat as one type in some scenarios, but as individual members in others (particularly when you will be pattern matching) lean towards using sealed traits.
    When you find yourself with a collection of similar objects or case classes that you want to treat as one type in some scenarios, but as individual members in others (particularly when you will be pattern matching) lean towards using sealed traits / type families.

    It will:

  5. mattbarackman revised this gist Jun 3, 2016. 1 changed file with 1 addition and 1 deletion.
    Original file line number Diff line number Diff line change
    @@ -80,7 +80,7 @@ object Yellow extends Color
    object Green extends Color
    ~~~

    We have an object `Car` with a `react` method that is pattern matching against the Color sealed trait.
    We have an object `Car` with a `react` method that is pattern matching against the `Color` sealed trait.

    ~~~scala
    // traffic_light/Car.scala
  6. mattbarackman revised this gist Jun 3, 2016. 1 changed file with 1 addition and 1 deletion.
    Original file line number Diff line number Diff line change
    @@ -58,7 +58,7 @@ As you can't extend a sealed trait outside of the file it was defined in, this w

    This guarantee provides a few advantages:

    1. It makes it easier to find and digest all the valid inputs to a method typed against the type family.
    1. It makes it easier to find and digest all the valid inputs to a method typed against a type family.
    2. It ensures that code in other files won't be creating unexpected inputs to methods typed against a type family.
    3. It allows the compiler to exhaustively check in a pattern match for all possible members of a type family.

  7. mattbarackman revised this gist Jun 3, 2016. 1 changed file with 1 addition and 1 deletion.
    Original file line number Diff line number Diff line change
    @@ -48,7 +48,7 @@ import Color
    object Blue extends Color
    ~~~

    As you can't extend a sealed trait outside of the file it was defined inGiven the definition of a sealed trait, this will result in a compiler error.
    As you can't extend a sealed trait outside of the file it was defined in, this will result in a compiler error.

    ~~~
    > sbt compile
  8. mattbarackman revised this gist Jun 3, 2016. 1 changed file with 1 addition and 1 deletion.
    Original file line number Diff line number Diff line change
    @@ -190,7 +190,7 @@ If you used a sealed trait and forgot to add the associated `BlinkingRed` case s

    ~~~
    > sbt compile
    [error] Car.scala:3: match may not be exhaustive.
    [error] Car.scala:4: match may not be exhaustive.
    [error] It would fail on the following input: BlinkingRed
    [error] def react(color: Color) = color match {
    ~~~
  9. mattbarackman revised this gist Jun 3, 2016. 1 changed file with 75 additions and 89 deletions.
    Original file line number Diff line number Diff line change
    @@ -1,38 +1,39 @@
    # Type Families, Sealed Traits, and Exhaustive Pattern Matching in Scala
    # Type Families, Sealed Traits, and Exhaustive Pattern Matching

    ## What is a Type Family?

    A collection of **objects** or **case classes** that share a **sealed trait**.

    In the example below, the *type family* would be a colelction of traffic light colors with `Red`, `Yellow`, and `Green` as member objects.
    In the example below, the *type family* would be a collection of traffic light colors with `Red`, `Yellow`, and `Green` as member objects.

    These are part of one family as they all extend the sealed trait `Color`.

    ~~~scala
    // TrafficLight.scala
    // traffic_light/Colors.scala

    object TrafficLight {
    sealed trait Color
    object Red extends Color
    object Yellow extends Color
    object Green extends Color
    }
    sealed trait Color
    object Red extends Color
    object Yellow extends Color
    object Green extends Color
    ~~~

    ## What is a Sealed Trait?

    A `trait` in this case acts as a shared interface between objects or case classes that extend it.

    They allow you to use the trait as a shared super-type in a method or variable signature. In the example below the trait `TrafficLight.Color` is the type signature of the parameter `color` in the Car#react method. Any of the members of the
    `TrafficLight.Color` type family (e.g. `TrafficLight.Red`, `TrafficLight.Green` or `TrafficeLight.Yellow`) would be valid inputs.
    They allow you to use the trait as a shared super-type in a method or variable signature. In the example below the trait `Color` is the type signature of the parameter `color` in the Car#react method. Any of the members of the
    `Color` type family (e.g. `Red`, `Green` or `Yellow`) would be valid inputs.

    ~~~scala
    //traffic_light/Car.scala

    object Car {
    def react(color: TrafficLight.Color) = color match {
    case TrafficLight.Red => "stop"
    case TrafficLight.Yellow => "slow"
    case TrafficLight.Green => "continue"
    def react(color: Color) = color match {
    case Red => "stop"
    case Yellow => "slow"
    case Green => "continue"
    }
    }
    ~~~


    @@ -41,28 +42,25 @@ object Car {
    Let's try and extend an object `Blue` in a file other than the one the sealed trait `Color` is defined in.

    ~~~scala
    // RogueTrafficLight.scala

    import TrafficLight.Color
    // traffic_light/NewColor.scala

    object RogueTrafficLight {
    object Blue extends Color
    }
    import Color
    object Blue extends Color
    ~~~

    As you can't extend a sealed trait outside of the file it was defined inGiven the definition of a sealed trait, this will result in a compiler error.

    ~~~
    > sbt compile
    [error] RogueTrafficLight.scala:6: illegal inheritance from sealed trait Color
    [error] NewColor.scala:4: illegal inheritance from sealed trait Color
    [error] object Blue extends Color
    ~~~

    This guarantee provides a few advantages:

    1. It makes it easier to find and digest all the valid inputs to a method typed against the type family.
    2. It ensures that code in other files won't be creating unexpected inputs to methods typed against a type family.
    3. It allows the compiler to exhaustively check against all possible members in a pattern match against the type family.
    3. It allows the compiler to exhaustively check in a pattern match for all possible members of a type family.

    Let's explore this third point further.

    @@ -74,130 +72,118 @@ Let's revisit what we have so far.
    We have a type family of traffic light colors with three members.

    ~~~scala
    // TrafficLight.scala
    // traffic_light/Colors.scala

    object TrafficLight {
    sealed trait Color
    object Red extends Color
    object Yellow extends Color
    object Green extends Color
    }
    sealed trait Color
    object Red extends Color
    object Yellow extends Color
    object Green extends Color
    ~~~

    We have an object `Car` with a `react` method that is pattern matching against the TrafficLight.Color sealed trait.
    We have an object `Car` with a `react` method that is pattern matching against the Color sealed trait.

    ~~~scala
    // Car.scala
    // traffic_light/Car.scala

    object Car {
    def react(color: TrafficLight.Color) = color match {
    case TrafficLight.Red => "stop"
    case TrafficLight.Yellow => "slow"
    case TrafficLight.Green => "continue"
    def react(color: Color) = color match {
    case Red => "stop"
    case Yellow => "slow"
    case Green => "continue"
    }
    }
    ~~~

    As you can see if we jump into the console, calling this method with the various colors returns a string indicating the reaction a car (it's a self-driving one) would have upon receiving the various inputs.
    As you can see when we jump into the console, calling this method with the various colors returns a string indicating the reaction a car (it's a self-driving one) would have upon receiving the various inputs.

    ~~~
    > sbt console
    scala> import Car
    scala> import TrafficLight
    scala> Car.react(TrafficLight.Red)
    scala> import traffic_light.{Car,Red}
    scala> Car.react(Red)
    res0: String = stop
    scala> Car.react(TrafficLight.Yellow)
    res1: String = slow
    scala> Car.react(TrafficLight.Green)
    res2: String = continue
    ~~~

    But let's say that we accidentally forgot to tell the car how to react in case of a `Red` traffic light.

    ~~~scala
    // Car.scala
    // traffic_light/Car.scala

    object Car {
    def react(color: TrafficLight.Color) = color match {
    // case TrafficLight.Red => "stop"
    case TrafficLight.Yellow => "slow"
    case TrafficLight.Green => "continue"
    def react(color: Color) = color match {
    // case Red => "stop"
    case Yellow => "slow"
    case Green => "continue"
    }
    }
    ~~~

    We would see a compiler warning telling us that `match may not be exhaustive`.
    If we're using a sealed trait, we would see a compiler warning telling us that `match may not be exhaustive`.

    ~~~
    > sbt compile
    [error] Car.scala:3: match may not be exhaustive.
    [error] Car.scala:4: match may not be exhaustive.
    [error] It would fail on the following input: Red
    [error] def react(color: TrafficLight.Color) = color match {
    [error] def react(color: Color) = color match {
    ~~~

    Why is this? If we're matching against a sealed trait, all the definitions for the members of the type family sharing that sealed trait are in one file. This makes it easy for the compiler to definitely know all of the members that are known to exist and can therefore give us a helpful error message at compile time when we forget to account for one in a pattern match.
    Why is this?

    By using a sealed trait, all the definitions for the members of a type family guaranteed to be in one file. This makes it easy for the compiler to definitely know all of the members that are known to exist. Therefore, it can give us a helpful error message at compile time when we forget to account for one or more members in a pattern match.

    But what happens if we don't use a sealed trait?

    ~~~scala
    // TrafficLight.scala
    // traffic_light/Colors.scala

    object TrafficLight {
    trait Color // sealed removed
    object Red extends Color
    object Yellow extends Color
    object Green extends Color
    }
    trait Color // sealed removed
    object Red extends Color
    object Yellow extends Color
    object Green extends Color
    ~~~

    Let's, again, leave the `Car` code with the `TrafficLight.Red` condition commented out.
    Let's, again, leave the `Car` code with the `Red` condition commented out.

    ~~~
    // Car.scala
    ~~~scala
    // traffic_light/Car.scala

    object Car {
    def react(color: TrafficLight.Color) = color match {
    // case TrafficLight.Red => "stop"
    case TrafficLight.Yellow => "slow"
    case TrafficLight.Green => "continue"
    def react(color: Color) = color match {
    // case Red => "stop"
    case Yellow => "slow"
    case Green => "continue"
    }
    }
    ~~~

    We'll see that we don't get a compiler error, but we do get a runtime error if we try and pass `TrafficLight.Red` into the `react` method.
    And now, it will actually compile, but when we hop into console we get a runtime error when we pass `Red` into the `react` method.

    ```
    > sbt console
    scala> import Car
    scala> import TrafficLight
    scala> Car.react(TrafficLight.Yellow)
    res0: String = slow
    scala> Car.react(TrafficLight.Green)
    res1: String = continue
    scala> Car.react(TrafficLight.Red)
    scala> import traffic_light.{Car, Red}
    scala> Car.react(Red)
    scala.MatchError: $TrafficLight$Red$@4bc2b213 (of class $TrafficLight$Red$)
    at $Car$.react(SimpleTypeFamilies.scala:4)
    ```

    Why is this different? The compiler does not have all the type family member definitions guaranteed to be in one place, so it won't presume it knows every member that could exist. So it won't even check for an exhaustive match at compile time, and therefore compiles fine. But it can and will blow up at runtime.

    So, the key takeaway here is that **using sealed traits can turn runtime errors into compiler errors** which are way less harmful as they can be more easily caught before code is deployed into production.


    ## Extending your Type Family

    This compile time error behavior is also helpful if you want to extend the type family.

    One might want to extend TrafficLight to include more colors. Perhaps you'd want to add a `BlinkingRed` option.
    One might want to extend your `Color` family to include more colors. Perhaps you'd want to add a `BlinkingRed` option.

    ~~~scala
    // TrafficLight.scala

    object TrafficLight {
    sealed trait Color
    object BlinkingRed extends Color // new member
    object Red extends Color
    object Yellow extends Color
    object Green extends Color
    }
    // traffic_light/Colors.scala

    sealed trait Color
    object BlinkingRed extends Color // new member
    object Red extends Color
    object Yellow extends Color
    object Green extends Color
    ~~~

    If you used a sealed trait and forgot to add the associated `BlinkingRed` case statement to the `Car.react` method, you would see a similar `match may not be exhaustive` error at compile time.
    @@ -206,20 +192,20 @@ If you used a sealed trait and forgot to add the associated `BlinkingRed` case s
    > sbt compile
    [error] Car.scala:3: match may not be exhaustive.
    [error] It would fail on the following input: BlinkingRed
    [error] def react(color: TrafficLight.Color) = color match {
    [error] def react(color: Color) = color match {
    ~~~

    So, using sealed traits can help ensure that as you extend your type family you are accounting for this new member everywhere that you are pattern matching against it.
    So, using sealed traits can help ensure that as you extend your type family you are accounting for new members everywhere that you may be pattern matching against the type family.

    ## Final Thoughts

    When you find yourself with a collection of similar objects or case classes that you want to treat as one type in some scenarios, but as individual members in others (particularly for pattern matching) lean towards using sealed traits.
    When you find yourself with a collection of similar objects or case classes that you want to treat as one type in some scenarios, but as individual members in others (particularly when you will be pattern matching) lean towards using sealed traits.

    It will:

    - Keep you from having to source dive
    - Limit the surface area of code that could create unexpected members as inputs
    - **And most importantly, it will create safer code by converting runtime errors into compiler errors when pattern matching against a type family**
    - And most importantly, it will create **safer code by converting runtime errors into compiler errors** when pattern matching against a type family


    ##Additional Reading:
  10. mattbarackman renamed this gist Jun 2, 2016. 1 changed file with 0 additions and 0 deletions.
  11. mattbarackman created this gist Jun 2, 2016.
    228 changes: 228 additions & 0 deletions Type Families, Sealed Traits, and Exhaustive Pattern Matching in Scala
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,228 @@
    # Type Families, Sealed Traits, and Exhaustive Pattern Matching in Scala

    ## What is a Type Family?

    A collection of **objects** or **case classes** that share a **sealed trait**.

    In the example below, the *type family* would be a colelction of traffic light colors with `Red`, `Yellow`, and `Green` as member objects.

    These are part of one family as they all extend the sealed trait `Color`.

    ~~~scala
    // TrafficLight.scala

    object TrafficLight {
    sealed trait Color
    object Red extends Color
    object Yellow extends Color
    object Green extends Color
    }
    ~~~

    ## What is a Sealed Trait?

    A `trait` in this case acts as a shared interface between objects or case classes that extend it.

    They allow you to use the trait as a shared super-type in a method or variable signature. In the example below the trait `TrafficLight.Color` is the type signature of the parameter `color` in the Car#react method. Any of the members of the
    `TrafficLight.Color` type family (e.g. `TrafficLight.Red`, `TrafficLight.Green` or `TrafficeLight.Yellow`) would be valid inputs.

    ~~~scala
    object Car {
    def react(color: TrafficLight.Color) = color match {
    case TrafficLight.Red => "stop"
    case TrafficLight.Yellow => "slow"
    case TrafficLight.Green => "continue"
    }
    ~~~


    `sealed` means that you can **only extend case classes or objects with this trait in the file in which the trait is defined.**

    Let's try and extend an object `Blue` in a file other than the one the sealed trait `Color` is defined in.

    ~~~scala
    // RogueTrafficLight.scala

    import TrafficLight.Color

    object RogueTrafficLight {
    object Blue extends Color
    }
    ~~~

    As you can't extend a sealed trait outside of the file it was defined inGiven the definition of a sealed trait, this will result in a compiler error.

    ~~~
    > sbt compile
    [error] RogueTrafficLight.scala:6: illegal inheritance from sealed trait Color
    [error] object Blue extends Color
    ~~~

    This guarantee provides a few advantages:

    1. It makes it easier to find and digest all the valid inputs to a method typed against the type family.
    2. It ensures that code in other files won't be creating unexpected inputs to methods typed against a type family.
    3. It allows the compiler to exhaustively check against all possible members in a pattern match against the type family.

    Let's explore this third point further.


    ##What is Exhaustive Pattern Matching?

    Let's revisit what we have so far.

    We have a type family of traffic light colors with three members.

    ~~~scala
    // TrafficLight.scala

    object TrafficLight {
    sealed trait Color
    object Red extends Color
    object Yellow extends Color
    object Green extends Color
    }
    ~~~

    We have an object `Car` with a `react` method that is pattern matching against the TrafficLight.Color sealed trait.

    ~~~scala
    // Car.scala

    object Car {
    def react(color: TrafficLight.Color) = color match {
    case TrafficLight.Red => "stop"
    case TrafficLight.Yellow => "slow"
    case TrafficLight.Green => "continue"
    }
    }
    ~~~

    As you can see if we jump into the console, calling this method with the various colors returns a string indicating the reaction a car (it's a self-driving one) would have upon receiving the various inputs.

    ~~~
    > sbt console
    scala> import Car
    scala> import TrafficLight
    scala> Car.react(TrafficLight.Red)
    res0: String = stop
    scala> Car.react(TrafficLight.Yellow)
    res1: String = slow
    scala> Car.react(TrafficLight.Green)
    res2: String = continue
    ~~~

    But let's say that we accidentally forgot to tell the car how to react in case of a `Red` traffic light.

    ~~~scala
    // Car.scala

    object Car {
    def react(color: TrafficLight.Color) = color match {
    // case TrafficLight.Red => "stop"
    case TrafficLight.Yellow => "slow"
    case TrafficLight.Green => "continue"
    }
    }
    ~~~

    We would see a compiler warning telling us that `match may not be exhaustive`.

    ~~~
    > sbt compile
    [error] Car.scala:3: match may not be exhaustive.
    [error] It would fail on the following input: Red
    [error] def react(color: TrafficLight.Color) = color match {
    ~~~

    Why is this? If we're matching against a sealed trait, all the definitions for the members of the type family sharing that sealed trait are in one file. This makes it easy for the compiler to definitely know all of the members that are known to exist and can therefore give us a helpful error message at compile time when we forget to account for one in a pattern match.

    But what happens if we don't use a sealed trait?

    ~~~scala
    // TrafficLight.scala

    object TrafficLight {
    trait Color // sealed removed
    object Red extends Color
    object Yellow extends Color
    object Green extends Color
    }
    ~~~

    Let's, again, leave the `Car` code with the `TrafficLight.Red` condition commented out.

    ~~~
    // Car.scala

    object Car {
    def react(color: TrafficLight.Color) = color match {
    // case TrafficLight.Red => "stop"
    case TrafficLight.Yellow => "slow"
    case TrafficLight.Green => "continue"
    }
    }
    ~~~

    We'll see that we don't get a compiler error, but we do get a runtime error if we try and pass `TrafficLight.Red` into the `react` method.

    ```
    > sbt console
    scala> import Car
    scala> import TrafficLight
    scala> Car.react(TrafficLight.Yellow)
    res0: String = slow
    scala> Car.react(TrafficLight.Green)
    res1: String = continue
    scala> Car.react(TrafficLight.Red)
    scala.MatchError: $TrafficLight$Red$@4bc2b213 (of class $TrafficLight$Red$)
    at $Car$.react(SimpleTypeFamilies.scala:4)
    ```

    So, the key takeaway here is that **using sealed traits can turn runtime errors into compiler errors** which are way less harmful as they can be more easily caught before code is deployed into production.


    ## Extending your Type Family

    This compile time error behavior is also helpful if you want to extend the type family.

    One might want to extend TrafficLight to include more colors. Perhaps you'd want to add a `BlinkingRed` option.

    ~~~scala
    // TrafficLight.scala

    object TrafficLight {
    sealed trait Color
    object BlinkingRed extends Color // new member
    object Red extends Color
    object Yellow extends Color
    object Green extends Color
    }
    ~~~

    If you used a sealed trait and forgot to add the associated `BlinkingRed` case statement to the `Car.react` method, you would see a similar `match may not be exhaustive` error at compile time.

    ~~~
    > sbt compile
    [error] Car.scala:3: match may not be exhaustive.
    [error] It would fail on the following input: BlinkingRed
    [error] def react(color: TrafficLight.Color) = color match {
    ~~~

    So, using sealed traits can help ensure that as you extend your type family you are accounting for this new member everywhere that you are pattern matching against it.

    ## Final Thoughts

    When you find yourself with a collection of similar objects or case classes that you want to treat as one type in some scenarios, but as individual members in others (particularly for pattern matching) lean towards using sealed traits.

    It will:

    - Keep you from having to source dive
    - Limit the surface area of code that could create unexpected members as inputs
    - **And most importantly, it will create safer code by converting runtime errors into compiler errors when pattern matching against a type family**


    ##Additional Reading:

    - [Everything You Ever Wanted to Know About Sealed Traits in Scala by underscore.io](http://underscore.io/blog/posts/2015/06/02/everything-about-sealed.html)