You are here

Game, Set, Match

Marc's picture
Submitted by Marc on Tue, 08/21/2018 - 08:58

One of the motivations in creating the Didactronic Framework was to learn new technology. Many ports of the framework have been started including using Python, Java, Clojure, and most recently Rust. Rust was an interesting options because of its promise of speed, safety and expressiveness. It seemed a good middle ground between imperative and functional programming. Since this is a completely new language and development paradigm for me (being primarily a C and Lisp hacker), the Rust framework will need to be refined over time to make use of the various constructs that are unique to that language. One such construct is the match form.

Match

The match form in Rust is similar to a switch statement in C or the cond form in Lisp. Essentially it will evaluate the given expression, and find a matching clause defined in the body of the match statement. This is illustrated in the listing that follows:

match rand::random::<u8>() {
    1 => println!( "Strike one" ),
    2 => println!( "Strike two!" ),
    3 => println!( "You're out!!" ),
    _ => println!( "Wait! What?!" )
}

In this example an 8-bit unsigned value is randomly selected. The result of this function is that passed to the match expression. The match expression will match the result with one of the values on the left side. When a match is found, the statement that follows the '=>' will be executed. The underscore is a placeholder which will match any value. Note that matches are attempted in the order in which the branches appear in the statement, therefore a random value of 3 will match the brach whose head is the value 3 before matching the underscore.

This same code snippet can be implemented in C as follows:

#include <time.h>
#include <stdio.h>
#include <stdlib.h>

int main( int argc, char* argv[] ) {
	srand( time(NULL) ) ;
	switch ( rand() ) {
	case 1: printf( "Strike one" ) ; break ;
	case 2: printf( "Strike two" ) ; break ;
	case 3: printf( "Strike three" ) ; break ;
	default: printf( "Wait! What?!" ) ;break ;
	}
	return 0 ;
}

Or as a Lisp cond form:

(let ((count (random))
      (cond
       ((= count 1) (print "Strike one!"))
       ((= count 2) (print "Strike two!"))
       ((= count 3) (print "Strike three!"))
       ('t (print "Wait! What?!"))
       )
      )
  )

The structure of each of these examples are fairly similar. However, where the Rust match statement really shines is in binding with sub-expressions.

Sub-Expression Matching

In experimenting with the Didactronic framework, I have created an example tic-tac-toe program to serve as a reference, as well as to test out the framework's design. Each player in the game is associated with a marker which is defined as an enumeration:

pub enum Marker {
    X,
    O
}

A configuration of the game board will represent the state of the game. The Configuration structure, which incidentally implements the State trait from the framework, is defined as follows:

pub struct Grid {
    states: RefCell<HashMap<u32,Rc<Configuration>>>,
}

pub struct Configuration {
    id: u32,
    last: Option<Marker>,
    value: f32,
    grid: Rc<Grid>,
}

Each configuration has associated therewith an ID which uniquely identifies it within the Grid environment and a value. The last field indicates the marker associated with the player who made the move that lead to the current Configuration. This is will be one of: Some(Marker::X), Some(Marker:O), or None. The match expression can be used to determine the marker of the next player to play as follows:

match configuration.last {
    Some(Player::X) => Player::O,
    _ => Player:X,
}

In this expression, Rust will attempt to bind the configuration's last field with the value Some(Player::X) and return Player::O, otherwise it will match the underscore and return Player::X.

This is very similar to the use of match from the previous section. A more intersting use could be in retrieving Configurations from the Grid's state set. When retrieving a Configuration via HashMap::get() function, either some state will be obtained, or None if no such state exists in the set. When a state is found, we want to clone its counted reference in order for the state set to retain ownership of the original state:

let state = match grid.borrow().get( &id ) {
    Some(s) => Rc::clone(&s),
    None => Rc::new(Configuration{ id, last, value: 0.0, grid: Rc::clone(&self.grid) })
}

In this example, Rust will attempt to bind the result of the get() operation with Some(s) where s is the unwrapped version of the Option container. By definition of the Grid structure, this will be a standard Rc<Configuration> which can be cloned. However, if the state is not found in the Grid, a new state will be created. This illustrates the sub-expression binding that is possible using Rust.

Conclusion

The match expression in Rust is a very powerful and expressive form. In many ways, it reminds me of my undergraduate days writing in Prolog; the bind logic seems to be very similar. Its ability to do sub-expression matching make it more powerful than the equivalent C (switch) or Lisp (cond) forms. That being said, I have not done any kind of performance analysis to determine how efficient this expression is. For the time being, I am satisfied with getting everything to work. There will be time to make it work faster once that particular summit has been reached. If and when such an analysis is performed, the results will assuredly appear in this blog. Until then, the hacking continues: Game (Tic-Tac-Toe), set (Grid.states), and match (in case the inspiration for the title was not clear).