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.

Solving the Blackjack Kata with Python Monads - Part 1
Since we just learned about the multi-value monad in the previous article, how about we use it to solve the Blackjack scoring kata?

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 as a = MultiValue({1, 11})
  • The map and flatmap 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 number 5 to the variable a above, the code will be a.map(lambda val: val + 5)
  • flatmap is used when doing an operation that involves another MultiValue, for example MultiValue({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:

  1. Adding an integer to a MultiValue
  2. 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 the MultiValue representation of it
  • possible_hand_values takes a hand of cards, converts each to its MultiValue representation and then adds up all the values using sum. Since the Ace has two possible values, this will actually give us all the possible totals for a hand of cards
  • hand_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 return None 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:

  1. You will get every new article as an email in your inbox, so you never miss an article
  2. You will be able to comment on all the posts, ask questions, etc
  3. Once in a while, I will be posting conference talk slides, longer form articles (such as this one), and other content as subscriber-only

Tagged in: