A little game, Phobos and more
bearophile
bearophileHUGS at lycos.com
Fri Apr 12 04:59:30 PDT 2013
In this post I use D and Phobos, to show some problems and to
suggest some improvements.
This little D program is one of the tasks of the Rosettacode
site, it allows to play a basic Rock-Paper-Scissors game on the
console against the computer:
- - - - - - - - - - - - - - - - -
import std.stdio, std.random, std.string, std.array;
immutable order = ["rock", "paper", "scissors"];
uint[string] choiceFrequency;
immutable string[string] whatBeats;
nothrow pure static this() {
whatBeats = ["paper": "scissors",
"scissors": "rock",
"rock": "paper"];
}
string checkWinner(in string a, in string b) pure nothrow {
if (b == whatBeats[a])
return b;
else if (a == whatBeats[b])
return a;
return "";
}
string getRandomChoice() {
if (choiceFrequency.length == 0)
return order[uniform(0, $)];
const choices = choiceFrequency.keys;
return whatBeats[choices[choiceFrequency.values.dice]];
}
void main() {
writeln("Rock-paper-scissors game");
while (true) {
write("Your choice: ");
immutable humanChoice = readln.strip.toLower;
if (humanChoice.empty)
break;
if (humanChoice !in whatBeats) {
writeln("Wrong input: ", humanChoice);
continue;
}
immutable compChoice = getRandomChoice;
write("Computer picked ", compChoice, ", ");
choiceFrequency[humanChoice]++;
immutable winner = checkWinner(humanChoice, compChoice);
if (winner.empty)
writeln("nobody wins!");
else
writeln(winner, " wins!");
}
}
The program has a choiceFrequency string-count associative array
that keeps counts of the human choices. The computer chooses to
play with a biased dice to beat the most probable (zero-order)
human choice (it's easy to add a first-order frequency table that
counts the adjacent pairs of human choices to improve a little
the computer playing skills).
That program is simple, but there are several small things to
improve or just discuss about.
- - - - - - - - - - - - - - - - -
1) Currently you can't write:
static this() nothrow pure {
You have to write:
nothrow pure static this() {
http://d.puremagic.com/issues/show_bug.cgi?id=6677
- - - - - - - - - -
2) This allocates a global immutable associative array:
immutable string[string] whatBeats;
nothrow pure static this() {
whatBeats = ["paper": "scissors",
"scissors": "rock",
"rock": "paper"];
}
I am glad that since some time whatBeats is allowed to be
immutable. But what we want to write is more naturally a global
variable like:
immutable whatBeats = ["paper": "scissors",
"scissors": "rock",
"rock": "paper"];
This problem is being worked on (I think there is a patch, I
don't remember what).
- - - - - - - - - -
3) Instead of this:
string getRandomChoice() {
if (choiceFrequency.length == 0)
I'd like to use std.array.empty:
if (choiceFrequency.empty)
http://d.puremagic.com/issues/show_bug.cgi?id=6409
- - - - - - - - - -
4) Choosing a single item randomly from a sequence is a very
common operation, so instead of this:
return order[uniform(0, $)];
A more natural function to use is "choice" as in Python:
return order.choice;
http://d.puremagic.com/issues/show_bug.cgi?id=4851
- - - - - - - - - -
5) This not efficient, and I'd like something shorter and faster:
const choices = choiceFrequency.keys;
return whatBeats[choices[choiceFrequency.values.dice]];
But let's skip this for now.
- - - - - - - - - -
6) I don't like to write strings more than once, because they are
magic constants:
immutable order = ["rock", "paper", "scissors"];
immutable string[string] whatBeats;
nothrow pure static this() {
whatBeats = ["paper": "scissors",
"scissors": "rock",
"rock": "paper"];
}
With a little function you don't need the associative array:
immutable order = ["rock", "paper", "scissors"];
string whatBeats(in string a) pure /*nothrow*/
in {
assert(order.canFind(a));
} body {
return order[(order.countUntil(a) + 1) % order.length];
}
Unfortunately whatBeats() can't be nothrow (why is countUntil on
the array not nothrow?), so checkWinner() can't be nothrow.
I am not so sure whatBeats() is better than the whatBeats[]
associative array. The function is more DRY, but it contains code
that needs to be unittested, while (in theory) that little
associative array just need one inspection to tell if it's
correct. In the end I have chosen the little function.
- - - - - - - - - -
7) It's generally a bad idea to repeat a raw type like "string"
everywhere, it's better to define an alias and use it instead:
alias Choice = string;
Even better is to use a strong typedef:
alias Choice = Typedef!string;
Thankfully now this Typedef works with limited changes to the
program, like:
immutable humanChoice = readln.strip.toLower.Choice;
- - - - - - - - - -
8) The function checkWinner() returns an empty string. Here a
safer program should use something like a Nullable!Choice:
Nullable!Choice checkWinner(in Choice a, in Choice b)
pure /*nothrow*/ {
if (b == whatBeats(a))
return typeof(return)(b);
else if (a == whatBeats(b))
return typeof(return)(a);
return typeof(return).init;
}
But it doesn't work, because a and b are constant (and scope),
while here the Nullable return type expects a not constant Choice.
If I try to define a Choice with a constant string to try to
solve the problem:
alias Choice = Typedef!(const string);
I receive an ICE that I have just filed in Bugzilla:
http://d.puremagic.com/issues/show_bug.cgi?id=9923
See also:
http://d.puremagic.com/issues/show_bug.cgi?id=9166
- - - - - - - - - -
9) Here I suggest helper functions to improve that code a little:
http://d.puremagic.com/issues/show_bug.cgi?id=9637
Nullable!Choice checkWinner(in Choice a, in Choice b)
pure /*nothrow*/ {
if (b == whatBeats(a))
return nullable(b);
else if (a == whatBeats(b))
return nullable(a);
return typeof(return).init;
}
But that should be done carefully, because here nullable() is
taking a Const(Choice) while the funciton returns a
Nullable!Choice that is not const, so a naive helper function
like this can't work (there are similar troubles with Tuple):
auto nullable(T)(T x) {
return Nullable!T(x);
}
Regarding this:
return typeof(return).init;
See also (but it's not very related):
http://d.puremagic.com/issues/show_bug.cgi?id=9636
- - - - - - - - - -
10) In a such small program it's probably acceptable to use
strings. But if I am using a staticall typed language and the
program gets larger I prefer to use enums, that give more static
safety. This is an obvious way to define a Choice:
enum Choice { rock, paper, scissors }
But after this change there are several small problems to solve,
listed below. Things get a bit messy.
- - - - - - - - - -
11) In D the index of arrays is just an integer, it can't be a
range of chars (like 'a'..'z') or an enum, like in Ada,
ObjectPascal, etc.
So this still defines an associative array, that for this program
is OK, but for a larger program with tens or hundreds of enums is
a waste of space and performance:
uint[Choice] choiceFrequency;
This is not wasteful (beside a bit of template bloat):
uint[EnumMembers!Choice.length] choiceFrequency;
(This code is uses EnumMembers because a D enum doesn't know how
many members it has:
http://d.puremagic.com/issues/show_bug.cgi?id=4997 ).
But that's about as statically safe as the associative array of
strings, because if Choice is not contigous, or if its members
have sparse values, it will not work:
enum Choice { rock, paper = 5, scissors = 10 }
It's not too much hard to write a static assert that calls a
compile-time function that varifies the enum to be contigous, but
I think this is overengeneering for this program.
- - - - - - - - - -
12) Given the Choice enum, how do you write a safe whatBeats()
function? A simple way to do it is to convert the enum into an
array:
Choice whatBeats(in Choice ch) pure /*nothrow*/ {
static immutable order = [EnumMembers!Choice];
return order[(order.countUntil(ch) + 1) % $];
}
In theory the items of an enum have an order. In theory you
should be able to iterate on the members (in D you can do it
converting the EnumMembers in an eager array or using a static
foreach), find the precedent and the successor of a member, and
tell if a member is the last or first one of an enum. In Ada
there are little accessors to do this, but in D you can write
little functions as FirstMember, LastMember, predMember,
nextMember:
http://d.puremagic.com/issues/show_bug.cgi?id=9924
With those functions you can write:
Choice whatBeats(in Choice ch) /*pure nothrow*/ {
if (ch == LastMember!Choice)
return FirstMember!Choice;
else
return nextMember(ch);
}
I have not used UCSF to avoid chashing with enum members. That's
why lot of time ago in Issue 4997 I have suggested to add a
single "meta" or "meta__" field to enums, and put all the enum
attributes inside (after) it. So only the meta__ enum field name
can clash.
In Issue 9924 Walter argues against little functions like
LastMember, etc.
- - - - - - - - - -
13) Thanks to a recent Phobos improvement it's now possible to
get a uniformly random enum. So getRandomChoice() is easy:
Choice getRandomChoice() /*nothrow*/ {
if (choiceFrequency[].reduce!q{a + b} == 0)
return uniform!Choice;
return [EnumMembers!Choice][choiceFrequency.dice].whatBeats;
}
And sum() will help:
http://d.puremagic.com/issues/show_bug.cgi?id=4725
http://d.puremagic.com/issues/show_bug.cgi?id=7934
https://github.com/D-Programming-Language/phobos/pull/1205
Choice getRandomChoice() /*nothrow*/ {
if (choiceFrequency.sum)
return uniform!Choice;
return [EnumMembers!Choice][choiceFrequency.dice].whatBeats;
}
- - - - - - - - - -
14) The main uses another Phobos improvement, that allows to
convert strings to enums:
void main() {
writeln("Rock-Paper-Scissors Game");
while (true) {
write("Your choice (empty to end game): ");
immutable humanChoiceStr = readln.strip.toLower;
if (humanChoiceStr.empty)
break;
Choice humanChoice;
try {
humanChoice = humanChoiceStr.to!Choice;
} catch (ConvException e) {
writeln("Wrong input: ", humanChoiceStr);
continue;
}
...
}
}
But here I'd like to use a maybeTo!() (it's just a wrapper that
calls to!() inside):
http://d.puremagic.com/issues/show_bug.cgi?id=6840
maybeTo is similar to to!(), but instead of using exceptions it
returns a Nullable. (In functional languages this is often the
preferred design, because pattern matching makes this design very
safe).
With it the code gets simpler:
Choice humanChoice = humanChoiceStr.maybeTo!Choice;
if (humanChoice.isNull) {
writeln("Wrong input: ", humanChoiceStr);
continue;
}
- - - - - - - - - -
15) You can't use this:
writeln(winner, " wins!");
As you see from this little test program:
import std.stdio: writeln;
import std.typecons: Nullable;
enum Foo { red, green, blue }
void main() {
Nullable!Foo(Foo.green).writeln;
}
That prints:
Nullable!(Foo)(green, false)
Is this a good printing for nullables? In Bugzilla I have
suggested something different.
So you need to use:
writeln(winner.get, " wins!");
- - - - - - - - - -
16) One possible whole program with enums (watch for bugs):
import std.stdio, std.random, std.string, std.array, std.typecons,
std.traits, std.conv, std.algorithm;
enum Choice { rock, paper, scissors }
immutable order = [EnumMembers!Choice];
uint[order.length] choiceFrequency;
Choice whatBeats(in Choice ch) pure /*nothrow*/ {
return order[(order.countUntil(ch) + 1) % $];
}
Nullable!Choice checkWinner(in Choice a, in Choice b)
pure /*nothrow*/ {
alias TResult = typeof(return);
if (b == whatBeats(a))
return TResult(b);
else if (a == whatBeats(b))
return TResult(a);
return TResult.init;
}
Choice getRandomChoice() /*nothrow*/ {
if (choiceFrequency[].reduce!q{a + b} == 0)
return uniform!Choice;
return order[choiceFrequency.dice].whatBeats;
}
void main() {
writeln("Rock-Paper-Scissors Game");
while (true) {
write("Your choice (empty to end game): ");
immutable humanChoiceStr = readln.strip.toLower;
if (humanChoiceStr.empty)
break;
Choice humanChoice;
try {
humanChoice = humanChoiceStr.to!Choice;
} catch (ConvException e) {
writeln("Wrong input: ", humanChoiceStr);
continue;
}
immutable compChoice = getRandomChoice;
write("Computer picked ", compChoice, ", ");
// Don't register the player choice until after
// the computer has made its choice.
choiceFrequency[humanChoice]++;
immutable winner = checkWinner(humanChoice, compChoice);
if (winner.isNull)
writeln("Nobody wins!");
else
writeln(winner.get, " wins!");
}
}
Bye,
bearophile
More information about the Digitalmars-d
mailing list