In this chapter we will develop a simple domain specific language (DSL) for rolling dice. Players of games such as Dungeon and Dragons are familiar with the DSL we will implement. An example of such DSL is
2 D20 + 1 D6 which means that we should roll two 20-faces dice and one 6 faces die. This tutorial shows how we can (1) simply reuse traditional operator such as
+, (2) develop an embedded DSL and (3) use class extensions (aka open classes).
Using the code browser, define a package named
Dice or any name your like.
initialization protocol, define the method
initialize as follows. It simply sets the default number of faces to 6.
Do not hesitate to add a class comment.
It is always empowering to verify that the code we write is always working as we defining it. For this purpose we will create a unit test. Remember unit testing was promoted by K. Beck first in Smalltalk the ancestor of Pharo. Nowadays this is a common practice but this is always useful to remember our roots!
So we define the class
DieTest as a subclass of
To roll a die we will use the method from Number
atRandom which draws randomly a
number between one and the receiver. For example
10 atRandom draws number between 1 to 10.
Therefore we define the method
roll as follows.
Now we can create an instance
Die new and send it the message
roll and get a result.
Die new inspect and then type in the bottom pane
You should get an inspector like the one shown in Figure 43.1. With it you can interact with a die by writing expression in the bottom pane.
We will define a test that verifies that rolling a new created dice with a default 6 faces only returns value comprised between 1 and 6. This is what the following test method is actually specifying.
Note that often it is better to define the test even before the code it tests. Why? Because you can think about the API of your objects and a scenario that illustrate their correct behavior. It helps you to program your solution.
We would like to get a simpler way to create Dice. For example we want to create a 20-faces die as follows:
Dice faces: 20. Let us define a test for it.
We define the class method
faces: as follows. It creates an instance then send the message
faces: to it and returns the instance.
This method is strictly equivalent to the one below. The trick is that
yourself is a simple method defined on
yourself returns the receiver of a message and the use of
; sends the message to the receiver of the previous message (here
yourself is sent to the object resulting from the execution of the expression
self new (which returns a new instance of the class
If you execute it will not work since we did not create yet the method
faces: this is now the time to define it.
Now your tests should run.
So even if the class
Die could implement more behavior, we are ready to implement a dice handle.
Let us define a new class
DiceHandle that represents a dice handle.
Here is the API that we would like to offer for now. We create a new instance of the handle then add some dice to it.
Of course we will define a test for this new class. We define the class
DiceHandleTest as follow.
We define a new test method as follows.
In fact we can do it better and add a new test method that verifies that we can even add two dice having the same number of faces.
This class defines one instance variable to hold dice it contains.
We simply initialize it so that its instance variable
dice contains an
Then we define a simple method to add a die to the list of dice of the handle.
Now you can execute the code snippet and inspect it. You should get an inspector as shown in Figure 43.2
Finally we should add the method
diceNumber to the
DiceHandle class to be able to get the number of dice of the handle. We just return the size of the dice collection.
Now your tests should run and this is good moment to save and publish your code.
Now when you open an inspector you cannot see well the dice that compose the dice handle. Click on the
dice instance variable and you will only get a list of
a Dice without further information.
So we will enhance the
printOn: method of the
Die class to provide more information. Here we simply add the number of faces surrounded by parenthesis.
Now we can define the rolling of a handle of dice by simply summing the dice rolls.
Now we can send the message
roll to a dice handle.
Now we are ready to offer a syntax following practice of role playing game, i.e., using
2 D20 to create a handle of two 20 faces dice. For this purpose we will define class extensions: we will define methods in the class
Integer but these methods will be only available when the package Dice will be loaded.
But first let us specify what we would like to obtain by writing a new test in the class
to always take any opportunity to write tests. When we execute
2 D20 we should get a new handle composed of two
dice and can verify that. This is what the method
testSimpleHandle is doing.
Verify that the test is not working! It is much more satisfactory to get a test running when it was not working before. Now define the method
D20 with a category name that is '*Dice' (if you named your package Dice). This method simply creates a new dice handle, add the correct number of dice to this handle and return it.
Now you test should pass and this is probably a good moment to save your work either by publishing your package to SmalltalkHub and to save your image.
Now we could do the same for the default dice with different faces number: 4, 6, 10, and 20.
But we should avoid duplicating logic and code. So first we will introduce a new method
D: and based on it we will define all the others
We have now a compact form to create dice and we are ready for the last part: the addition of handles.
Now we can simply support the addition of handles. But of course let's write a test first.
We will define a method
+ on the HandleDice class. In other languages this is often not possible or is based on operator overloading. In Pharo
+ is just a message as any other, therefore we can define it on the classes we want.
Now we should ask ourself what is the semantics of adding two handles. Should we modify the receiver of the expression or create a new one. We preferred a more functional style and choose to create a third one.
+ creates a new handle then add to it the dice of the receiver and the one of the handle passed as argument to the message. Finally we return it.
Now we can execute the method
(2 D20 + 1 D6) roll nicely and start playing role playing games, of course.
This chapter illustrates how to create a small DSL based on the definition of some domain classes (here
DiceHandle) and the extension of core class such
It shows that in Pharo we can use usual operators to express natural models.