In the previous article, we saw how we can solve the Blackjack Kata using monads with a functional programming approach. In this article, we will solve the same problem, once again using monads but leveraging Python's Object Oriented data model. Python, being a multi-paradigm programming language is well suited for this kind of mix-and-match approach.
If you haven't read the previous article, I suggest taking a look first as it explains the problem statement.
The Multi-Value Monad
Just like before, we start off with the multi-value monad.
from itertools import chain
class MultiValue:
def __init__(self, values):
self.values = set(values)
def map(self, fn):
out = {fn(val) for val in self.values}
return MultiValue(out)
def flatmap(self, fn):
out = (fn(val).values for val in self.values)
return MultiValue(chain(*out))
One of the key operations we need to do to solve the blackjack kata is be able to add up a set of values. Some of those values might be numbers, while others might be MultiValue
objects.
A quick summary for those who are reading this article without reading the previous ones:
- The class
MultiValue
above allows us to create items that can have multiple values. For example, the Ace in Blackjack game can have the value 1 or the value 11. Both are valid and you can choose the value which is most beneficial for you. We represent this asa = MultiValue({1, 11})
- The
map
andflatmap
functions are used to perform operations on these values.map
is used when the output of an operation is a regular value. Example, if I want to add the number5
to the variablea
above, the code will bea.map(lambda val: val + 5)
flatmap
is used when doing an operation that involves anotherMultiValue
, for exampleMultiValue({1, 2}) + MultiValue({3, 4})
Again for more details, consult the multi-value monad article.
Summing up a hand
Consider a hand of cards [2, 5, 7, A]
. Since A
can take the value 1
or 11
, the sum of this hand could be 15
(when A
is 1
) or 25
(when A
is 11
). If we represent A
as MultiValue({1, 11})
then the hand can be represented as hand = [2, 5, 7, MultiValue({1, 11})]
.
Ideally, we can use the sum()
built-in function to calculate the sum of this list, but it won't work because of the MultiValue
object in there.
In the functional programming approach of the previous article, we wrote the functions add
, add_mv_and_int
and add_multivalue
which helped us to add up the list.
We follow the same approach here, but instead of creating standalone functions, we are going to implement the __add__
dunder method to MultiValue
so that it supports the +
operator directly.
Here is what the skeleton looks like
class MultiValue:
...
def __add__(self, other):
...
def __radd__(self, other):
return self + other
Note that we also implement __radd__
so that MultiValue() + x
as well as x + MultiValue()
works.
Just like in the previous article, there are two cases we need to consider:
- Adding an integer to a
MultiValue
- Adding two
MultiValue
objects
In case we are adding MultiValue
to an integer, then we can use the map
method to do the operation.
class MultiValue:
...
def __add__(self, other):
match other:
case int():
return self.map(lambda a: a + other)
def __radd__(self, other):
return self + other
When we are adding two MultiValue
together, then we can use the flatmap
method to do the operation
class MultiValue:
...
def __add__(self, other):
match other:
case int():
return self.map(lambda a: a + other)
case MultiValue():
return self.flatmap(lambda a: a + other)
def __radd__(self, other):
return self + other
Check out the previous article for an in-depth explanation of how the map
and flatmap
work in this situation.
We can test this out
>>> MultiValue({2, 3}) + 10
MultiValue({12, 13})
>>> 10 + MultiValue({2, 3})
MultiValue({12, 13})
>>> MultiValue({2, 3}) + MultiValue({10, 20})
MultiValue({12, 13, 22, 23})
Now that we have implemented support for the +
operator for the MultiValue
class, we can just use the sum()
built-in function normally, even if we have MultiValue
objects in the list. The code below is mostly the same as the one from the previous article, except we use the sum
function to add up all the values instead of using reduce
def get_card_value(card):
if card == 'A':
return MultiValue({1, 11})
elif card in ('J', 'Q', 'K'):
return MultiValue({10})
else:
return MultiValue({card})
def possible_hand_values(hand: list[CardFace]) -> MultiValue:
values = [get_card_value(card) for card in hand]
return sum(values, MultiValue({0}))
def hand_value(hand: list[CardFace]) -> int | None:
try:
possible_values = possible_hand_values(hand).values
valid_values = {value for value in possible_values if value <= 21}
return max(valid_values)
except ValueError:
return None
Here is what this code does:
get_card_value
takes a card and returns theMultiValue
representation of itpossible_hand_values
takes a hand of cards, converts each to itsMultiValue
representation and then adds up all the values usingsum
. Since the Ace has two possible values, this will actually give us all the possible totals for a hand of cardshand_value
takes the list of all possible hand values, filters out those hands that bust, and returns the maximum of whatever is remaining. It will returnNone
if every possible hand value is a bust
Summary
That brings us to the end of this Blackjack Kata using the MultiValue
monad. In these two articles, we saw how we can use the monad to abstract items that could take on multiple values. In the Blackjack example, that was the Ace which could take on the value of 1
or 11
whichever gave a better hand value.
Normally dealing with such variables leads to complex code, but with the monad abstraction, we barely need to think about it.
Furthermore, by hooking on to python's ability for operator overloading, we were able to make the MultiValue
support the +
operator, leading to some very clean code. In the end, we could use sum
to add up all the values in the hand of cards as if it was just a list of numbers, when actually there were MultiValue
in there.
In essence, this is the power of abstraction. It frees the developer to write the core algorithm at a high level ("Sum up the values in the hand") without having to think about the internal details ("Some cards in the hand take single values, some take on multiple values")
Did you like this article?
If you liked this article, consider subscribing to this site. Subscribing is free.
Why subscribe? Here are three reasons:
- You will get every new article as an email in your inbox, so you never miss an article
- You will be able to comment on all the posts, ask questions, etc
- Once in a while, I will be posting conference talk slides, longer form articles (such as this one), and other content as subscriber-only