Skip to content

Motivation for creating structures

Simple game example

Imagine a game between two players and the score for winning a game is stored in a string. Positive values are won by player 1, zeros are draws and negative values are won by player 2:

results_str = '-7 3 0 5 -4 0 0 -1'
If we want to find out how many times player 2 has won, we can convert this string to a list of numbers and make a comparison using the definitions of the game:
results = generate_result(results_str);
player2 = sum(results < 0); %(1)

function result = generate_result(results_str)
    as_arr = split(results_str, ' '); %(2)
    result = str2double(results_str_arr); %(3)
end

  1. Count the number of true
  2. Split at every space...
  3. ... then convert to number
On str2double

By default matlab assigns all numbers to double (double precision floating point, often called f64), but scores are only integers. It might be ideal to convert the scores into i32

function as_i32 = generate_result(results_str)
    as_arr = split(results_str, ' ');% Split at every space, 
    as_f64 = str2double(as_arr);    % then convert to number
    as_i32 = i32(as_f64);
end

This is a reasonable solution. The problem is that reading this code snippet without a full explanation of the game makes it really hard to understand what is being done and why.

Sometimes a good variable name is enough

We could represent the different states of the game with well-named variables.

If we already have the results when we want to define the states, we could use

results = generate_result(results_str);

player1 = results > 0;
player2 = results < 0;
is_draw = results == 0;

n_victories_player2 = sum(player2);

If they are being defined before we read the results, we might want a small function we can call when results is eventually known. You can find a longer explanation of anonymous functions (is_player1_victory = @(x) x > 0;) below.

is_player1_victory = @(x) x > 0;
is_player2_victory = @(x) x < 0;
is_draw = @(x) x == 0;

% ... do other things
results = generate_result(results_str); 
player2 = is_player2_victory(results);
player1 = is_player1_victory(results);
n_victories_player2 = sum(player2);

What are anonymous functions?

Anonymous functions have the following syntax:

x = [1 2 3];
five = 5;
mean_add_five = @(x) mean(x) + five;

mean_add_five(x)

There are two main benefits to it:

  1. They can be used to create concise, inline function definitions for simple expressions
  2. Unlike normal functions, they "capture the environment". We define five outside of the function body and the anonymous function can call it.

Here is the same function definition. What do you expect would happen if you were to run this code?

x = [1 2 3];
five = 5;
mean_add_five(x);

function mean_add_five(x)
    mean(x) + five
end

Growing complexity

We also want to know the scores for each player and in which games they won

score_player2 = sum(results(player2)); % (1)
games_won_player2 = find(player2);

score_player1 = sum(results(player1));
games_won_player1 = find(player1);
  1. Sums elements of results where player2 is true. These are numbers, unlike sum(results < 0) where we counted booleans.

And if we don't want to repeat ourselves:

[score_player2, games_won_player2] = player_stats(player2)

function [score, games_won] = player_stats(player)
    score = sum(results(player));
    games_won = find(player);
end

Think about the data we're recording: For every player we are recording different pieces of information, but they are completely disconnected from each other. If we had many more pieces of information, we might struggle with keeping track of them. Often we want data that refers to the same object grouped together into structures (usually shortened to struct).

We can update the code above to:

player2_info = player_info(player2)

function info = player_info(player)
    score = sum(results(player));
    games_won = find(player);

    info.score = score;
    info.games_won = games_won;
end

Accessing each piece of information is as simple as calling

player2_info.score

Armed with this knowledge, let's try to use what we have learnt to model a game.


Modelling the world

I'm unfamiliar with object oriented programming

Matlab provides two ways to organise complex data: unnamed structures and classes. We saw struct just above.

Classes are significantly more complex to write, but they come with some interesting advantages:

  • Can attach functions to classes, which we call methods. For example, a Dog class might have a bark() method.
  • Can attach data to classes, which we call properties. For example, a Dog class might have a name property.

The syntax is as follows

classdef Dog

    properties % (1)!
        Name
    end

    methods % (2)!
                        % (3)!
        function self = Dog(name)    
            self.Name = name; % (4)!

        end
                     % (5)!
        function bark(self)    
            disp("woof!")
        end

    end

end

my_dog_name = 'Fido';
my_dog = Dog(my_dog_name);
my_dog.bark();

  1. The data you want tied to these objects
  2. The functions you want tied to these objects
  3. How to create a Dog. Known as a "Constructor", it has the same name as the class
  4. Information that is necessary to create a dog (i.e. what we defined in properties). Here, self says we're attaching data to the Name property of the object we're currently creating.
  5. Here, self means that this function requires a defined dog. In other words, "requires an instance of the class".

Note: You can just write independent functions to replicate this functionality. The advantage is thinking about "I want to make my dog bark", as opposed to "I want a bark, let me choose my dog for it".

For the sake of completeness, we will model this game as receiving some set of player inputs, applying rules and getting game results.

The details of handling player inputs and applying rules are irrelevant, but in the next section, where we translate these concepts over to a concrete example, thinking about the entire flow of the program is very helpful.

% Game.m
classdef Game
    properties
        Results
    end

    methods
        function self = Game(players_input)
            % Process the input into some structure we're happy with
            input_parsed = parse_player_input(players_input);

            % Apply all the game logic
            results = calculate_results(input_parsed); 
            self.Results = results;
        end
    end
end

% (1)!
function parsed = parse_player_input(input)
    parsed = split(input, ' '); % (2)!
end
function results = calculate_results(input)
    results = str2double(input); % (3)!
end
  1. Here we define how to start a game and turn the input from players into results.
  2. Imagine this being a lot more complicated
  3. Imagine this being a lot more complicated

We can create a new instance of the game and even look into the results stored within:

% main.m
results_str = '-7 3 0 5 -4 0 0 -1';
my_game = Game(results_str);
disp(my_game.Results);

Internally we are storing the state of the game just as we did before, so maybe we want to look at the information for both players with just one function call

classdef Game
%... properties, etc
    methods
    %... Other functions
        function [player1, player2] = players_info(self)
            % Player 1:
            is_player1 = self.Results > 0;
            player1 = player_info(is_player1);

            % Player 2:
            is_player2 = self.Results < 0;
            player2 = player_info(is_player2);
        end
    end
end

% Reusing the helper function we created earlier
function [info] = player_info(player)
    score = sum(results(player));
    games_won = find(player);

    info.score = score;
    info.games_won = games_won;
end

which means we can find information about our players with a pretty sleek interface:

[player1, player2] = my_game.players_info();
disp(player1.score);

Key takeaways

  • Try to make your code a reflection of the real system you are modelling.
  • Use types that handle the complexity of your system. Some times, descriptive variable names are sufficient. Other times, complex classes that are harder to code might make usage much easier.
  • Iterate! Growing complexity may require rewriting your functions.

In the next sections we will apply this same thought process to improve some of the workflows that exists within my lab.