Pattern matching is a powerful tool to work with data structures. It’s like a switch statement in other languages, but with a bit more pizazz. Each case of a match statement defines the shape, or pattern, of the data that will match the case.
Matching Enum Types
Let’s start by looking at a simple enum type.
1 2 3 4 5 6 7 8 9 10 11 12
moduleMain enumPizzaTopping {Cheese,Pepperoni,Peppers,Pineapple} let topping=Peppers match(topping){ Cheese=>print("Would it really be pizza without it?"), Pepperoni=>print("An instant classic."), Peppers=>print("For those who like to spice things up."), Pineapple=>print("You do you.") }
This example prints out a fun message depending on which pizza topping is bound to topping. Each case is separated by a comma.
You can also surround the body of your match case with curly braces to include more statements.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
moduleMain enumPizzaTopping {Cheese,Pepperoni,Peppers,Pineapple} let getPrice=(topping)=>{ match(topping){ Cheese=>4, Pepperoni=>6, Peppers=>{ print("Peppers are on sale this week!") 3 }, Pineapple=>9 } } getPrice(Peppers)
Default Cases
If we only care about some of the cases, we can use an underscore pattern to match all other possible cases.
1 2 3 4 5 6 7 8 9 10
moduleMain enumPizzaTopping {Cheese,Pepperoni,Peppers,Pineapple} let topping=Peppers match(topping){ Cheese=>print("Would it really be pizza without it?"), _=>print("That's cool, but the cheese makes the pizza.") }
If we were to omit the last underscore “catch-all” case, the Grain compiler would complain that we’re missing cases for Pepperoni, Peppers, and Pineapple.
Nested Match Patterns
We can nest match patterns as deeply as we’d like. If we sold both one-topping pizzas and calzones, we may want to single out a particular pizza/topping combo:
1 2 3 4 5 6 7 8 9 10 11 12
moduleMain enumTopping {Cheese,Pepperoni,Peppers,Pineapple} enumMenu {Pizza(Topping),Calzone(Topping)} let item=Calzone(Peppers) match(item){ Calzone(Peppers)=>print("These are half off this week."), Pizza(Cheese)=>print("We never discount this item."), _=>print("No current specials.") }
We can also use an underscore anywhere within a pattern to match remaining cases.
1 2 3 4 5 6 7 8 9 10 11 12
moduleMain enumTopping {Cheese,Pepperoni,Peppers,Pineapple} enumMenu {Pizza(Topping),Calzone(Topping)} let item=Calzone(Peppers) match(item){ Calzone(Peppers)=>print("These are half off this week."), Calzone(_)=>print("Enjoy 10% off, on us."), _=>print("No current specials.") }
Bindings in Match Patterns
We can bind portions of a match pattern to a name and use that bound value in the body the corresponding case.
moduleMain enumTopping {Cheese,Pepperoni,Peppers,Pineapple} enumMenu { Calzone(Topping), Pizza{ stuffedCrust: Bool, topping: Topping } } let checkSpecials=topping=>{ match(topping){ Cheese=>true, Pepperoni=>true, _=>false, } } let item=Calzone(Peppers) match(item){ Calzone(topping)=>{ if(checkSpecials(topping)){ print("These are half off this week.") } }, Pizza{stuffedCrust,topping}=>{ if(!stuffedCrust&&checkSpecials(topping)){ print("These are 30% off this week.") } } }
One common use of pattern matching and binding is working with Option and Result enums.
1 2 3 4 5 6 7 8 9 10 11 12 13
moduleMain let opt=Some("I'm a match") match(opt){ Some(msg)=>print(msg), None=>print("No match found") } let result=Err("This is an error") match(result){ Ok(msg)=>print(msg), Err(e)=>print(e) }
The first example will print I'm a match because the Option has a value.
The second example will print This is an error because the Result contains an Err.
Matching Record Types
Like most Grain data structures, pattern matching can also be done on records.
1 2 3 4 5 6 7 8 9
moduleMain recordPerson {name:String,age:Number} let person={name:"Steve",age:25} match(person){ {name,age}=>print(name) }
If we don’t care about some of the record fields, we can use an underscore to tell the Grain compiler that we’ve intentionally left those fields out.
1 2 3 4 5 6 7 8 9
moduleMain recordPerson {name:String,age:Number} let person={name:"Steve",age:25} match(person){ {name,_}=>print(name) }
Nested Matching Within Records
Things are a bit more interesting when we have data structures nested within records.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
moduleMain enumTopping {Cheese,Pepperoni,Peppers,Pineapple} enumOrder {Pizza(Topping),Calzone(Topping)} recordPerson {name:String,order:Order} let person={name:"Steve",order:Calzone(Pepperoni)} match(person){ {order:Pizza(_),_}=>print("All pizzas are great here."), {order:Calzone(Peppers),_}=>print("Someone with great taste!"), {order:_,_}=>print("Yep, that's an order.") }
Matching Tuples
Pattern matching can also be performed on tuples.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
moduleMain enumTopping {Cheese,Pepperoni,Peppers,Pineapple} enumOrder {Pizza(Topping),Calzone(Topping)} enumOrderType {DineIn,Takeaway} let order=(Calzone(Pineapple),DineIn) match(order){ (Calzone(Pineapple), DineIn)=>{ print("We can't let the other customers see this.") }, (_, Takeaway)=>print("Box it up to go."), (_, DineIn)=>print("Clean off table 5.") }
Matching Lists
Often with lists, we want to do something with the first element of a list and then do some further processing with the rest of the list.
In this example, we define an add2 function to increment each value in a list of numbers by 2.
The [first, ...rest] pattern creates bindings for the first element in the list, and a list containing the rest of the elements. In the body of this case, the [first + 2, ...add2(rest)] expression creates a new list. The first element of the result is the sum of 2 and the first element of the original list. The remaining elements of the list are the result of a recursive call to the add2 function on the rest of the original list.
We can also match on multiple elements at the beginning of a list:
1 2 3 4 5 6 7 8
moduleMain let list=[1,2,3] match(list){ [first,second,..._]=>first+second, _=>fail"List contained fewer than 2 elements" }
Finally, matches can also be performed on lists with specific lengths.
1 2 3 4 5 6 7 8 9 10 11
moduleMain let list=[1,2,3] match(list){ []=>print("List contains no elements"), [_]=>print("List contains one element"), [_,_]=>print("List contains two elements"), [_,_,_]=>print("List contains three elements"), _=>print("List contains more than 3 elements") }
Match Guards
Sometimes you want to be more specific about conditions for matching. You can use a match guard to place more specific limitations on a match case.
You can think of a match guard as a combination of a match and an if. A guard allows you to add an additional statement to qualify whether the case matches.
1 2 3 4 5 6 7 8
moduleMain let myNumber=Some(123) match(myNumber){ Some(val)whenval>100=>print("More than 100"), Some(val)=>print("Less than or equal to 100"), None=>print("Nothing at all") }
In the example above, myNumber is an Option whose value is Some(123). But our match statement has three cases instead of two. The first case adds a guard: Some(val) when val > 100. This case will only be matched when the Option is Some() and the value inside that option is a number greater than 100.
The next case, Some(val), will match any Some() value that is not matched by the guarded version.
Order is important when using guards. The first case to match is the one that will be used. For example, we could change the above example to this:
1 2 3 4 5 6 7 8
moduleMain let myNumber=Some(123) match(myNumber){ Some(val)=>print("Less than or equal to 100"), Some(val)whenval>100=>print("More than 100"), None=>print("Nothing at all") }
In this version, regardless of the size of myNumber, it would always match the first case.
Even default cases can have guards.
1 2 3 4 5 6 7 8 9
moduleMain let myNumber=Some(99) let isTuesday=true match(myNumber){ Some(val)whenval>100=>print("Greater than 100"), _whenisTuesday=>print("It's Tuesday"), _=>print("Nothing else matched") }
In the example above, if myNumber is greater than 100 (which it is not), then the program prints Greater than 100. Otherwise, if isTuesday is true, then it prints It's Tuesday (regardless of whether myNumber is a Some() or a None). Finally, if nothing else matches, the program prints Nothing else matched.