We have written articles covering a number of programming languages in this blog so far including Python, Go, Erlang, Clojure and Shell-script. Today in what may be one of the last posts of the year, we will match up our programming skills with the newest but kind of rusty kid on the block who specializes in system programming - Rust .

This is aimed to be an introductory tutorial covering one of the most powerful programming paradigms offered by Rust, namely pattern matching .

What you need

To run through this tutorial, you need the rust compiler installed on your system. You can follow the steps in the rust install page for getting this done.

We are not going to develop any crate during this tutorial, so setting up cargo is optional. Instead we will use the rust compiler, rustc directly to build our code.

Once rust is installed, check your version using,

$ rustc --version
rustc 1.29.2 (17a9dc751 2018-10-05)

Enums

Enumerations or enums are a common programming construct in many languages. Enums allow the programmer to represent a set of related values belonging to the same type inside a single definition, rather than defining the values one by one.

For example, in C you can create types for integers, say zero to five as follows.

#define ZERO 0
#define ONE 1
#define TWO 2
#define THREE 3
#define FOUR 4
#define FIVE 5

However, this is less verbosely and more succinctly captured as an enum.

enum INTEGERS {
    ZERO,
    ONE,
    TWO,
    THREE,
    FOUR,
    FIVE
}

You can then use the enums along with the switch statement to match a value and take actions. For example, the following code reads an integer argument from the command line and prints a message if it matches any of the defined enum values.

/* enums.c */
#include <stdio.h>

enum INTEGERS {
    ZERO,
    ONE,
    TWO,
    THREE,
    FOUR,
    FIVE
};

int main(int argc, char** argv)
{
  int i;

  /* Read command line argument to an integer */
  sscanf(argv[1], "%d", &i);
  
  switch (i) {
  case ZERO:
    printf("I got nothing!\n");
    break;
  case ONE:
    printf("I got one.\n");
    break;
  case TWO:
    printf("I got two!\n");
    break;
  case THREE:
    printf("Three is trouble\n");
    break;
  case FOUR:
    printf("Four in a row!\n");
    break;
  case FIVE:
    printf("Five to go\n");
    break;
  }
    
}

Note: In C, the first member of an enum defaults to a value of zero.

You can compile and run this program using a C compiler, and run it with various arguments as follows:

$ gcc -o enums enums.c
$ 
$ ./enums 0
I got nothing
$ ./enums 3
Three is trouble
$ ./enums 4
Four in a row!

The idea of an enumeration is now hopefully clear to you.

Guess we spend enough time on that introductory C part. Let us move over to our core topic which is Rust.

Matching Traffic Lights

Enums in Rust along with the match keyword allows a form of pattern matching which is similar to the switch statement in C, but more powerful.

Let us look at an example. In the following example, we are defining an enumeration for a traffic signal and matching it to an input value to print appropriate messages.

// pattern_example.rs
enum TrafficSignal
{
    GREEN,
    ORANGE,
    RED,
    OFF
}
    
fn traffic_action(signal: TrafficSignal)
{
    match signal {
        TrafficSignal::GREEN => println!("Green => Traffic should go now"),
        TrafficSignal::RED => println!("Red => Traffic should stop now"),           
        TrafficSignal::ORANGE => println!("Orange => Traffic should proceed with caution now"),
        TrafficSignal::OFF => println!("Off => Traffic is managed by the traffic police now"),
    }
}

fn main()
{

    let mut signal = TrafficSignal::RED;
    traffic_action(signal);

    signal = TrafficSignal::GREEN;
    traffic_action(signal);

    signal = TrafficSignal::ORANGE;
    traffic_action(signal);

    signal = TrafficSignal::OFF;
    traffic_action(signal);

}

First compile this program using rustc

$ rustc pattern_example.rs

This produces the pattern_example executable. Run it.

$ ./pattern_example
Red => Traffic should stop now
Green => Traffic should go now
Orange => Traffic should proceed with caution now
Off => Traffic is managed by the traffic police now

For those somewhat familiar with Rust, the program should be straight-forward. Anyway, here are the steps in some detail.

  1. We define an enumeration named TrafficSignal representing a traffic signal with values RED, GREEN, ORANGE and OFF.
  2. We define a function named traffic_action which accepts a TrafficSignal enum and using pattern matching, matches it to each of the values, printing an appropriate message to the standard output.
  3. In the main function, we create a mutable variable named signal, assign it various values of the enum and call traffic_action on it.

However, enums in Rust are not just container of data, they can also hold methods. We can rewrite the above program in a more elegant fashion by moving the function into the TrafficSignal enum as a method.

Signal, Lights, Action!

Here is the rewritten version which moves the function traffic_signal inside the enum as a method. This is achieved by the impl block.

// pattern_example2.rs
enum TrafficSignal
{
    GREEN,
    ORANGE,
    RED,
    OFF
}
    
impl TrafficSignal {

    fn traffic_action(&self) -> String {
        match *self {
            TrafficSignal::GREEN => String::from("Green => Traffic should go now"),
            TrafficSignal::RED => String::from("Red => Traffic should stop now"),           
            TrafficSignal::ORANGE => String::from("Orange => Traffic should proceed with caution now"),
            TrafficSignal::OFF => String::from("Off => Traffic is managed by the traffic police now")
        }
    }
}

fn main()
{

    let mut signal = TrafficSignal::RED;
    println!("{}", signal.traffic_action());

    signal = TrafficSignal::GREEN;
    println!("{}", signal.traffic_action());

    signal = TrafficSignal::ORANGE;
    println!("{}", signal.traffic_action());

    signal = TrafficSignal::OFF;
    println!("{}", signal.traffic_action());

}

The output of the program is the same as the one before. However the main differences are,

  1. The traffic_signal function is moved to the TrafficSignal enumeration as a method via the impl block. The method takes a reference to self as the explicit first argument. (This is similar to Python classes accepting an explicit reference to self as the standard first argument in their instance methods).
  2. The match is made on *self since self is a reference. The asterisk * used here is known as the indirection operator which operates on a reference and de-references it.
  3. The match expression returns a String value than printing it. We print this value in the main function.

Signals with Embedded Action

Enums in Rust need not be just pre-defined values that can be used for simple matches. They can also be configured with arguments - which can be any Rust type, allowing for more complex matching.

In this version of our traffic signal enumerations, we create a variation of the enum which accepts String values and use these values to return matching messages as traffic actions.

The main change is a re-definition of the enum from,

enum TrafficSignal
{
    GREEN,
    ORANGE,
    RED,
    OFF
}

to,

enum TrafficSignal
{
    GREEN(String),
    ORANGE(String),
    RED(String),
    OFF(String)
}

The idea here is to generate the enumerations with an embedded action message which will be returned by the match expression.

The traffic_action method for this is,

impl TrafficSignal {

    
    fn traffic_action(&self) -> &String
    {
        match *self {
            TrafficSignal::GREEN(ref msg) |
            TrafficSignal::RED(ref msg) |
            TrafficSignal::ORANGE(ref msg) |
            TrafficSignal::OFF(ref msg) => msg
        }
    }
}

What changed here ?

  1. The enums now take an explicit String argument so the match should also accept one. This is done by the ref msg argument in the match expression which stands for a reference to this string. Hence the function signature is modified to indicate it returns a reference to String (as &String).
  2. Since the message is now embedded in the enum itself, all match cases can be combined into one. This is done using the | operator, which combines the matches for all defined enum values into one block, returning the message string reference.

The main function is now,

fn main()
{

    let mut signal = TrafficSignal::RED(String::from("Red => Traffic should stop now"));
    println!("{}", signal.traffic_action());

    signal = TrafficSignal::GREEN(String::from("Green => Traffic should go now"));
    println!("{}", signal.traffic_action());

    signal = TrafficSignal::ORANGE(String::from("Orange => Traffic should proceed with caution now"));
    println!("{}", signal.traffic_action());

    signal = TrafficSignal::OFF(String::from("Off => Traffic is managed by the traffic police now"));
    println!("{}", signal.traffic_action());
    
}

You can see the enums now are created with their string messages whose references are then further returned by the pattern matching function and printed to the console.

Here is the full program.

// pattern_example3.rs
enum TrafficSignal
{
    GREEN(String),
    ORANGE(String),
    RED(String),
    OFF(String)
}

impl TrafficSignal {

    
    fn traffic_action(&self) -> &String
    {
        match *self {
            TrafficSignal::GREEN(ref msg) |
            TrafficSignal::RED(ref msg) |
            TrafficSignal::ORANGE(ref msg) |
            TrafficSignal::OFF(ref msg) => msg
        }
    }
}
    
fn main()
{

    let mut signal = TrafficSignal::RED(String::from("Red => Traffic should stop now"));
    println!("{}", signal.traffic_action());

    signal = TrafficSignal::GREEN(String::from("Green => Traffic should go now"));
    println!("{}", signal.traffic_action());

    signal = TrafficSignal::ORANGE(String::from("Orange => Traffic should proceed with caution now"));
    println!("{}", signal.traffic_action());    

    signal = TrafficSignal::OFF(String::from("Off => Traffic is managed by the traffic police now"));
    println!("{}", signal.traffic_action());    
    
}

The output of this version is the same as the other two, so I am not repeating it here.

Summary

Rust is an excellent system programming language with support for advanced enumerations with powerful pattern matching operations. In this tutorial, we uncovered the basics of pattern matching in Rust starting from simple enumerations to one accepting String arguments.

Pattern matching in Rust is about much more than just enumerations. A pattern can be matched over other constructs like structs, arrays, tuples, wildcards and literals.

If time permits, we will come up with more examples of Rust and pattern matching in future articles. Hope you learned something here today and enjoy your time with Rust!


Note that name and e-mail are required for posting comments