Section 23.2 Creating a Module
To begin with, let’s look at a module template:
module PlayingCards
## definitions of types and functions
end
As you can see above, a module has a name (in this case
PlayingCards) and ends with the keyword
end. We will next put a number of types and functions inside the module, but in order for someone loading the module, you need to export anything to be used. We have used the naming convention of capital letters (often called Pascal case) for a Module name.
We will take all of the structs, constructors and functions associated with playing cards that we developed earlier and place them in the module.
module PlayingCards
import Base.show
export Card, Hand, isFullHouse
ranks = ['A','2','3','4','5','6','7','8','9','T','J','Q','K']
suits = ['\u2660','\u2661','\u2662','\u2663']
struct Card
rank::Int
suit::Int
# construct a card based on the rank and suit
function Card(r::Int,s::Int)
1 <= r <=13 || throw(ArgumentError("The rank must be an integer between 1 and 13."))
1 <= s <= 4 || throw(ArgumentError("The suit must be an integer between 1 and 4."))
new(r,s)
end
# construct a card based on the number in a deck
function Card(i::Int)
1 <= i <= 52 || throw(ArgumentError("The argument must be an integer between 1 and 52"))
new(mod1(i,13), div(i-1,13)+1)
end
# construct a card based on a string representation of the card
function Card(str::String)
length(str)==2 || throw(ArgumentError("The string should only be 2 characters"))
local r = findfirst(a->a==str[1],ranks)
r != nothing && 1 <= r <= 13 || throw(ArgumentError(string("The first character should be one of ",join(ranks,","))))
local s = findfirst(a->a==str[2],suits)
s != nothing && 1<= s <= 4 || throw(ArgumentError(string("The second character should be one of ",join(suits,","))))
new(r,s)
end
end
struct Hand
cards::Array{Card,1}
# constructors
Hand(cards::Array{Card,1}) = new(cards)
Hand(cards::Array{String,1}) = new(map(Card,cards))
Hand(s::String) = new(map(Card,map(String,split(s,','))))
end
function Base.show(io::IO, c::Card)
print(io,string(ranks[c.rank],suits[c.suit]))
end
function Base.show(io::IO, h::Hand)
print(io,string("[",join(map(c->string(ranks[c.rank],suits[c.suit]),h.cards),",")),"]")
end
function isFullHouse(h::Hand)
local cranks=sort(map(c->c.rank,h.cards))
cranks[2]==cranks[1] && cranks[5]==cranks[4] && (cranks[3]==cranks[2] || cranks[4]==cranks[3]) &&
cranks[2] != cranks[4]
end
end #module PlayingCards
Open a new text file in jupyter and copy-paste the above module into the empty file. It will need to be called
PlayingCards.jl and should be in the same directory as the file you’re working on. Note: in most editors, it is important to have the
.jl suffix on the filename so the coloring/formatting is correct for the file.
To load this module, we will do:
includet("PlayingCards.jl")
using .PlayingCards
where the
includet function is in the
Revise package. It includes the file and the t stands for tracking. What will happen if you update anything in the file and then make any call to those that are in the package, you will get the new revised code without restarting the kernel or reloading the package.
Here are some things to note about the module:
-
Line 3:
import Base.show will import the
show command from
Base, and will be needed to develop good format for cards and hands.
-
Line 5:
export Card, Hand, isFullHouse says what will be available if the module is loaded.
-
Staring on Line 10, the
Card struct is defined as we saw in
Chapter 12. We have added an additional constructor starting at line 28, that creates a card based on a string.
-
Staring on Line 39, the
Hand struct is defined as we saw in
Chapter 12. This one is a bit different in that chapter because we have added to additional constructors.
-
the functions
Base.show functions for both
Card and
Hand are defined on lines 48 and 52. The command
Base.show is automatically loaded, so these didn’t need to be exported.
-
the
isFullHouse functions is defined starting on line 56.
Once you have saved and successfully loaded the module, let’s test it a bit. First let’s build some of the hands that we did in
Chapter 20:
fh1 = Hand([Card(4,1),Card(4,3),Card(4,4),Card(7,1),Card(7,2)])
fh2 = Hand([Card(4,1),Card(4,3),Card(7,4),Card(7,1),Card(7,2)])
fh3 = Hand([Card(2,1),Card(4,3),Card(4,4),Card(7,1),Card(7,2)])
and the last hand should look like
[2♠,4♢,4♣,7♠,7♡].
Check Your Understanding 23.1.
(a)
Add the
isOneSuit,
hasRun,
royalFlush,
isTwoPair and
runTrials functions from
Chapter 20 to the module.
(b)
Add the
royalFlush,
isTwoPair and
runTrials functions to be exported (top of the module).
(c)
Create a hand that is a Royal Flush poker hand and one that is not.
(d)
Create a hand that is a Two Pair poker hand and one that is not.
(e)
Test that the
runTrials function is working by testing the functions.
Section 23.3 Unit Tests
Writing a module is important, but making sure it does what it is supposed to is just as important. At first, when you start writing code, you typically make sure there are no bugs, but after time, code changes and you’re not sure that the code still works. The notion of a unit test is a set of tests to determine if you get out from the code what you expect. This is a language-independent idea and should be created once you write any level of substantial code.
To run a test in Julia, first import the
Test package:
To do a test, we’ll use the
@test macro and it’s a good idea to check out the
Julia documentation on Test. For this module, let’s first test that the constructor is working using the
isa function.
@test isa(Card(1,4),Card)
should return
Test Passed. Recall also that we can pass in an integer between 1 and 52, so
should also return
Test Passed. If we try to create a card that is not valid, then we won’t get a Test Passed. For example:
will return
Error During Test. A better way to test for this is using the
@test_throws method to test if an error is thrown:
@test_throws ArgumentError Card(78)
Here’s another nice test that will check if the two different ways to create Cards are the same. For this we will need a way to test if two Cards are equal. Add the following to the Playing Cards module.
isequal(x::Card,y::Card) = x.rank==y.rank && x.suit==y.suit
and test if two cards are the same with
@test isequal(Card(2,3),Card(28))
and this returns
Test Passed.
Check Your Understanding 23.2.
Write a test that the
isFullHouse method is working. To do this:
(a)
Create a hand that is a full house, called
h1. Run
@test isFullHouse(h1).
(b)
Create another hand called
h2 that is a full house and test it.
(c)
Create a third hand called
h3 that is not a full house and test it. To get the test to return passed, perform
@test !isFullHouse(h3).