Lecture 2.2: Pattern matching and Pairs
Some more fun with functions: type inference
Static typing is excellent.
The type is inferred. OCaml can use the information offloat_of_int to capture the correct information.
In contrast, the following will be is a type error:
Of course, as you have seen, we can always annotate our functions. DO THIS! It will help you understand the mismatch between your intention and the compiler output.
Pattern matching
Pattern matching allows us to perform a case analysis on a piece of data. We can use it for primitive types, but the real power of this feature is in dealing with composite datatypes. Let's look at a simple example of an if with multiple alternatives:
let foo (x : int) : string =
if x = 1 then "one" else
if x = 2 then "two" else
if x = 3 then "three" else
"greater than three" ;;
Since we are checking for specific values, we can achieve the same much more concisely with pattern matching:
let foo (x : int) : string =
match x with
| 1 -> "one"
| 2 -> "two"
| 3 -> "three"
| _ -> "greater than three" ;;
What are the pros and cons between these two approaches? A collection of if-/switch- statements might be straightforward, but with matching, we let the OCaml compiler to do the work for us as the type gives it more information about the structure of the data. Let's next take a look at pattern matching in the context of pairs.
Tuples
Up to now, we have been mostly dealing with built-in types: ints, floats, bools, strings. The only type composed of other types were functions. We often want to composed data types to create new types.
A basic datatype that allows us to package values of different types together are tuples. For example here is a pair (a 2-tuple) of ints:
Note
Strictly speaking, the parentheses are not needed:
but we will always use them.We can see that the type of a pair of two ints is int * int, which corresponds to the concept of Cartesian products. Given a pair, we can get the components by using fst and snd:
utop # let x = (2, 3);;
val x : int * int = (2, 3)
utop # fst x;;
- : int = 2
utop # snd x;;
- : int = 3
Additionally, we can destruct tuples through let-bindings:
We can have tuples with more than two elements
We can also deconstruct tuples using pattern matching
let teaches_2800 (name_instr : string * bool) : bool =
match name_instr with
| ("sam", true) -> true
| ("mira", false) -> false
| ("max", false) -> false
| ("aryn", false) -> false
| (_ , false) -> false
;;
let is_instructor (name_instr : string * bool) : bool =
match name_instr with
| (_, b) -> b
;;
To help our programs make sense, it is often useful to give custom names to existing types. Let's make an alias to a type.
type point2d = int * int
let shift_x (p : point2d) (delta : int) : point2d =
match p with
| (x, y) -> (x + delta, y)
;;
Just like deconstructing tuples in a let-binding, we can deconstruct them in function arguments:
type point2d = int * int
(* good examples: *)
let shift_x ((x, y) : point2d) (delta : int) : point2d = (x + delta, y) ;;
let shift_x (x, y : point2d) (delta : int) : point2d = (x + delta, y) ;;
You can also do compound pattern matching
let shift (x, y : point2d) (dx, dy : point2d) : point2d = (x + dx, y + dy)
let shift (xy : point2d) (dxdy : point2d) : point2d =
match (xy, dxdy) with
| ((x, y), (dx, dy)) -> (x + dx, y + dy)
Basic Abstract Datatypes (ADTs)
Let's look at few more examples of ADTs, and how you might use them. Suppose you're creating a graphics application that lets you create various shapes and give them colors. First, we can define colors by an enumeration:
Here, the type is just an enumeration between six choices, without any data attached. While we could representcolor with an integer (e.g., 0 for red, 1 for green, and so on), using an explicit enumeration with names is useful for code clarity.
For example, let's write a function to compute complementary colors:
let complementary (c : color) =
match c with
| Red -> Green
| Green -> Red
| Blue -> Orange
| Orange -> Blue
| Yellow -> Purple
| Purple -> Yellow
(As you might have noticed, this is another example of an involutive function.)
Next, let's define a type for shapes:
type shape = | Rectangle of int * int (* width and height *)
| Circle of int (* radius *)
| Square of int (* side length *)
Finally, we want to define a type that holds both a color and a shape. While we could do this with a pair type (color * shape), there is another way: by using a record:
This defines a record, which must hold a field b_color of type color, and b_shape of type shape. Let's create one and analyze it: