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'
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
- Count the number of
true - Split at every space...
- ... 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:
- They can be used to create concise, inline function definitions for simple expressions
- Unlike normal functions, they "capture the environment". We define
fiveoutside 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);
- Sums elements of
resultswhere player2 is true. These are numbers, unlikesum(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, aDogclass might have abark()method. - Can attach data to classes, which we call
properties. For example, aDogclass might have anameproperty.
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();
- The data you want tied to these objects
- The functions you want tied to these objects
- How to create a Dog. Known as a "Constructor", it has the same name as the class
- Information that is necessary to create a dog (i.e. what we defined in
properties). Here,selfsays we're attaching data to theNameproperty of the object we're currently creating. - Here,
selfmeans 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
- Here we define how to start a game and turn the input from players into results.
- Imagine this being a lot more complicated
- 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.