Intro to Unison datatypes and pattern matching

We've been applying pattern matching toliteralvalues at this point. But pattern matching is frequently done on more complicated types, where we can use the cases of the pattern to decompose the data type into its constituent parts. Here we'll talk about Unison data types in the context of pattern matching.

Your first Unison data type

Let's take a look at a pre-made data type,Either.Either is used to represent situations in which a value can be one type or another.

In Unison, Either is defined in the base library like this (we'll break down what these parts mean shortly):

structural type Either a b
structural type Either a b
  = lib.base.Either.Right b
  | lib.base.Either.Left a

The keywordtypeindicates that we're looking at a type definition, as opposed to aterm definition.Unison types are given a modifier ofstructuralorunique--structuralhere means that types which share Either's structure are treated as identical to Either and therefore are interchangeable in Unison code, even if they're given a different name.Eitheris the name we've given to our type, and the two lettersaandbare type parameters.

Type parameters in Unison are lowercase by convention. For example:

type Box a
type Box a = languageReference.destructuringBinds.Box.Box a
You can think ofaandbas placeholders which represent any type. When we construct a value of type Either, we "fill in" the placeholder.

On the right hand side of the equals are thedata constructorsof the type. We use data constructors to create a value of the type being described, so to create anEitherwe have two options:LeftorRight.They're separated by pipes|.When you see the pipe think "or."

📌
In summary: you can read the line
structural type Either a b
structural type Either a b
  = lib.base.Either.Right b
  | lib.base.Either.Left a
asEitheris aLeftcontaining anaor aRightcontaining ab.Unison data types are often composed by slotting other Unison types into one data constructor or another. Sometimes those types are not specified, like foraandbinEither,and sometimes they're pinned down to a concrete type.

We'll return toexplore more in-depth about data typeslater.

Decomposing data types with pattern matching

With our whirlwind intro to the parts of a data type behind us, we'll return to how to pattern match on the different data constructors of a given type.

Let's say we wanted a function to tell us which utensils should be paired with a lunch order. We'll use the following types:

type Lunch
type Lunch
  = fundamentals.controlFlow.patternMatching.Lunch.Soup Text
  | fundamentals.controlFlow.patternMatching.Lunch.Salad Text
  | fundamentals.controlFlow.patternMatching.Lunch.Mystery
      Text Boolean
type Utensil
type Utensil
  = fundamentals.controlFlow.patternMatching.Utensil.Fork
  | fundamentals.controlFlow.patternMatching.Utensil.Knife
  | fundamentals.controlFlow.patternMatching.Utensil.Spoon

Our function should take in a typeLunchas an argument and return aListof typeUtensil.We know that there are only three ways to make a value of typeLunch,so we match on the data constructor name followed by the number of fields that the constructor contains.

placeSetting : Lunch -> [Utensil]
placeSetting = cases
  Soup soupName   -> [Spoon]
  Salad saladName -> [Fork, Knife]
  _               -> [Spoon, Fork, Knife]

Pattern matching on the data constructors of the type enables us to inspect and make use of the values they contain. In the example above we don't end up using the variables that are bound to the fields in the data, so we could have also represented them as underscores, likeSoup _ -> [Spoon],but we can imagine a function where that would become important:

placeSetting : Lunch -> [Utensil]
placeSetting = cases
  Soup "Hearty Chunky Soup"   -> [Fork, Spoon]
  Soup _                      -> [Spoon]
  Salad _                     -> [Fork, Knife]
  Mystery mysteryMeal isAlive ->
    use Text ==
    if (mysteryMeal == "Giant Squid") && isAlive then [Knife]
    else [Spoon, Fork, Knife]
  _                           -> [Spoon, Fork, Knife]

The first case is an example of how to combine a literal pattern match with a data constructor, and the second and third cases are an example of how to match on any value thatSouporSaladdata constructor might enclose. Our last case extracts the values being provided to theMysterydata constructor aspattern match variablesfor use on the right.

Note, the underscores above represent the fact that the value being provided to the data constructor isn't important for the logic of our expression on the right. The underscores do, however, need to be present. Every parameter to the data constructor needs to be represented in the pattern either by a variable, as in ourMysterycase, or by an underscore, otherwise Unison will return a patternaritymismatch error.