paint-brush
Writing an Infinitely Long Essay Using State Pattern in Pythonby@aayn
2,341 reads
2,341 reads

Writing an Infinitely Long Essay Using State Pattern in Python

by Aayush NaikDecember 27th, 2023
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

State design pattern is state machines in object-oriented code. We use the pattern to create an essay/sentence generator.
featured image - Writing an Infinitely Long Essay Using State Pattern in Python
Aayush Naik HackerNoon profile picture

Let’s write an infinitely long essay! But that’s an impossible task. However, we can create a process that, if run forever, would generate an infinitely long essay. Close enough.


Now, you can obviously generate a long and repetitive essay with a single line of Python code:


>>> "This is water. " * 20
'This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. '


Yaawn… boooring! Instead, we will generate a much more interesting essay using the State design pattern in this article.


First, we will understand what state machines are and how they’re related to the State design pattern. Next, we will create a state machine that can generate a (reasonably) interesting and infinite essay. Then, we will briefly go over the state design pattern. Finally, we will translate that state machine into object-oriented code using the state design pattern.


Software design patterns are effective ways to solve commonly occurring problems. When applied appropriately, software design patterns like the state pattern can help you write better scalable, maintainable, and testable software.

State Machine

In essence, the State design pattern translates a State Machine into object-oriented code.


If you aren’t familiar with state machines, it’s a pretty simple concept. A state machine has states and transitions. States are certain properties of our system of interest, and state transitions are actions that change these properties and thereby also cause a state change.


Since I have a Robotics background (among other things) and because state machines are used extensively in Robotics, I’ll use a simple example of a robot vacuum cleaner to illustrate how state machines work.


State machine to drive a robot vacuum cleaner.


The state machine diagram paints an intuitive picture of how the robot operates, even if you’ve never encountered state machines. Let’s go over this operation step by step.


  1. The robot starts in the Docked state (the black dot indicates the starting state).
  2. If the robot detects its battery is low, it begins charging itself (Charging state) until its battery is full. Once the battery is full, it returns to the Docked state.
  3. In the Docked state, if the robot detects that the floor is dirty (and its battery is not low), it starts cleaning the floor (Cleaning state).
  4. In the Cleaning state, if the robot gets low on battery, then it goes to charge itself. And if the floor is clean, the robot returns to its dock (Docked state).


Thus, our robot vacuum cleaner has three states — Docked, Cleaning, and Charging — and has transitions based on sensory detection of the floor and its battery.


Simple state machine for infinite essay

Now that we understand state machines at a basic level let’s create a state machine capable of writing an infinite essay.


State machine to generate infinite essay - version 1


Above is a state machine that uses English grammar to generate an essay comprised of short, simple sentences. I promise we’ll get to a more interesting version very soon, but this should serve as a good starting point for understanding. Let’s go over how it works.


  1. Starting in the Noun state, we generate a noun by picking from some pre-defined list of nouns. Let’s say our noun is “The World”. (sentence so far: “The World”)
  2. Then we end up in the Verb state, generating a verb next (say, “barks”). (sentence so far: “The World barks”)
  3. We generate an adjective (say, “red”) in the Adjective state. (sentence so far: “The World barks red”)
  4. Then, in the Endmark state, we generate one of the terminating punctuation marks, say “!”. (sentence: “The World barks red!”)
  5. Finally, we’re back in the Noun state to generate our next sentence in the essay.


This state machine might generate a (nonsensical) essay that looks like this.


The World barks red! Cousin Harry rains foul? The tigers shimmer fun. …


Non-deterministic state machine for infinite essay

Although “non-deterministic” sounds complicated, for our purposes, it just means adding in some randomness. Essentially we’re adding a kind of a coin toss before transitioning to some of the states. You’ll see what I mean.

Non-deterministic state machine to generate infinite essay - version 2


The non-deterministic state machine above is very similar to the one before. The only differences are:

  • Negations are words like “no” or “not,” and conjunctions are words like “and” and “but.”
  • In the Verb state, we generate a verb and then we toss a coin. If it lands heads (50% probability), we go to the Negation state; otherwise we go to the Adjective state.
  • Similarly, in the Adjective state, we generate an adjective and then toss a coin. If heads, we go to the Conjunction state; if it is tails, then we go to the Endmark state.


With the introduction of randomness, negation, and conjunctions, we can now generate more interesting and variable-length sentences.


State Design Pattern

Now, let’s understand how the state design pattern works. Again, remember that we’re trying to translate a state machine to object-oriented code.


In the essay generation state machine, observe that every state needs to do two things.

  1. Perform some action. In this case, generating a word (noun, adjective, etc.).
  2. Transition to the next state. From Noun to Verb, and so on.


From the point of view of a particular state, there is nothing else that it needs to know about or do. Instead of being bogged down by the complexity of the entire system — all its states and transitions — we can just focus on one state at a time. In my view, this kind of isolation and decoupling is the biggest selling point of the State pattern.


Below, we have a UML diagram for the State design pattern. There is some context in which each of the states operates, illustrated by the Context class. The context object has a private state attribute, which it uses to call the current state to perform its action. Each state implements a State interface with methods for performing its action or operation and returning the next state.


State design pattern UML diagram


If we map this onto the essay generation example, the UML diagram looks like this.


State pattern implemented for essay generation


WordState is now an abstract class (indicated by italics) instead of an interface. Abstract classes can have some abstract (not implemented) methods and attributes, while others can be defined. Interfaces are totally abstract: all their methods are abstract. I made this change because generateWord implementation is the same across all the states, and avoiding duplicate code is good.


Let’s break down each of the attributes and methods above. In the EssayContext class, we have:

  • state: Reference to the current WordState object.
  • essayBody: List of all the words generated so far.
  • setState(): Setter to change the state attribute.
  • addWord(): Method to add the next word to the essay body.
  • generateEssay(): We call this method to generate our essay; we stop when the essayBody has length greater than length.
  • printEssay(): Returns a string of the generated essay.


In the abstract class WordState, we have:

  • wordList: Abstract property (indicated by italics) for a list of words from which we choose words to generate.
  • generateWord(): Method which adds generated word to the essay context.
  • nextState(): Abstract method for returning the next state.


We’ll use NounState as a representative example for all the other concrete states inherited from WordState.

  • wordList: A list of nouns from which we choose words to generate.
  • nextState(): Returns the next state.


Now, we have everything we need to actually implement this in code. Let’s go ahead and do just that!


Python Code

Let’s first write the EssayContext class in a file called essay_context.py. We'll ditch camel case and switch to snake case because, well, Python is a... snake (sorry).


from word_state import WordState


class EssayContext:
    def __init__(self, state: WordState):
        self.state = state
        self.essay_body: list[str] = []

    def set_state(self, state: WordState):
        self.state = state

    def add_word(self, word: str):
        self.essay_body.append(word)

    def generate_essay(self, length: int):
        while len(self.essay_body) < length:
            self.state.generate_word(self)

    def print_essay(self) -> str:
        return " ".join(self.essay_body)


Then, let’s add the states in a file called word_state.py.


import abc
import numpy as np


class WordState(abc.ABC):
    word_list: list[str]

    @classmethod
    def generate_word(cls, context: "EssayContext"):
        word = np.random.choice(cls.word_list)
        context.add_word(word)
        context.set_state(cls.next_state())

    @classmethod
    @abc.abstractmethod
    def next_state(cls) -> "WordState":
        pass


class NounState(WordState):
    word_list: list[str] = ["everything", "nothing"]

    @classmethod
    def next_state(cls):
        return VerbState


class VerbState(WordState):
    word_list: list[str] = ["is", "was", "will be"]

    @classmethod
    def next_state(cls):
        heads = np.random.rand() < 0.5
        if heads:
            return NegationState
        return AdjectiveState


class NegationState(WordState):
    word_list: list[str] = ["not"]

    @classmethod
    def next_state(cls):
        return AdjectiveState


class AdjectiveState(WordState):
    word_list: list[str] = ["fantastic", "terrible"]

    @classmethod
    def next_state(cls):
        heads = np.random.rand() < 0.5
        if heads:
            return ConjunctionState
        return EndmarkState


class ConjunctionState(WordState):
    word_list: list[str] = ["and", "but"]

    @classmethod
    def next_state(cls):
        return NounState


class EndmarkState(WordState):
    word_list: list[str] = [".", "!"]

    @classmethod
    def next_state(cls):
        return NounState


Finally, let’s add code to run everything in main.py.


from essay_context import EssayContext
from word_state import NounState


if __name__ == '__main__':
    ctx = EssayContext(NounState)
    ctx.generate_essay(100)
    print(ctx.print_essay())


Running python main.py gives us the following output (different each time because of non-determinism):


'everything is not terrible and nothing was terrible ! everything will be not fantastic but everything is fantastic . everything will be fantastic . nothing will be fantastic and nothing will be terrible ! everything was not fantastic and everything will be not terrible . everything was terrible . nothing was terrible but nothing will be fantastic ! nothing is not terrible . nothing was not fantastic but everything was not fantastic ! everything will be not fantastic but everything will be terrible ! everything will be not fantastic . everything is fantastic but nothing will be not terrible ! everything will be not fantastic but nothing was not fantastic !'


Not bad for such a simple system! We can also extend the various word lists or add more states to make the essay generation more sophisticated. We could even introduce some LLM APIs to take our essays to the next level.


Final thoughts

State machines and the State pattern are a great fit to model and create systems with a well-defined notion of a “state.” That is, there are specific behaviors and properties associated with each state. The robot vacuum cleaner is cleaning, docked or charging. Your TV can be ON or OFF, and the TV remote buttons will act differently based on the TV’s state.


It’s also a good fit for generating or identifying sequences with a well-defined pattern. This applies to our essay generation example.


Finally, you might ask, “What’s the point of all this?” Why did we go through all the trouble defining the various states and classes to generate this “infinite” essay? We could have written 20 (or fewer) lines of Python code to achieve the same behavior.


The short answer is for better scalability.


Imagine if, instead of just three or five states, we had 50 or 500 states. This isn’t hyperbole; real enterprise systems do have that level of complexity. Suddenly, the State pattern seems much more appealing because of its decoupling and isolation. We can just focus on one state at a time without having to keep the entire system in our heads. It’s easier to introduce changes since one state won’t affect others. It also allows for easier unit testing, a big part of a scalable and maintainable system.


Ultimately, the State pattern is not just about managing states; like all design patterns, it’s a blueprint for building systems that are as scalable and maintainable as they are sophisticated.