Instantiations of the Object Pattern

By Max Woerner Chase

In this post:


Working on Homunculus and the new script for publishing this blog, I ran across two different "classless" uses of the object pattern in Python. To be specific about what the object pattern is:

Pattern Name and Classification
Object
Intent
Given (only) a cohesive collection of program state, have a means of obtaining functions that alter their behavior based on the state, or alter the state.
Also Known As
Traits and typeclasses, probably.
Motivation (Forces)
A collection of variables or a data structure can provide all of the data needed for some computational task, but does not afford any particular means of interacting with the data. It cannot enforce invariants, and if different collections of data could be put to the same purpose, consumers of the data must be aware of which kind of collection they are acting on, to use the proper functions. By providing a means to obtain or create the specific functions meant for a collection of state, that collection of state assumes the responsibility of maintaining invariants and implementing behavior; all callers have merely to use the object protocols to obtain the desired behavior.
Applicability

In some languages, the use of this pattern is basically mandatory to accomplish anything. In freer contexts, the criteria for applicability are:

  1. There are multiple underlying implementations that could conceivably be provided to the same consuming code, OR at least one implementation involves state subject to invariants.
  2. This form of dispatch is actually supported by the language.
Structure

A survey of some possible implementations:

  • Module structure:
    Modules have functions and variables.
    The caller resolves a function in the module scope, then calls it. The function implicitly has access to the module scope, and can read and write to it.
  • Closure structure:
    Closures close over a function scope, which contains variables.
    The caller invokes a closure, which resolves variables in its current scope, possibly mutating them before returning.
  • Class structure:
    Objects are instances of a class. Objects have attributes, and classes have methods.
    The caller looks up the class of the object, resolves the method from the class, passes the object into the method, which operates on the object, then returns a result.
Participants
  • Module structure:
    1. Module
    2. Values
    3. Functions
  • Closure structure:
    1. Function scope
    2. Values
    3. Closures
  • Class structure:
    1. Class
    2. Instance
    3. Values
    4. Methods
Collaboration
  • Module structure: The module contains values and functions. The functions are scoped to or passed the module, which allows them to read and mutate the variables.
  • Closure structure: The closures close over the function scope, which contains the values. This allows them to read and mutate the variables.
  • Class structure: The object is an instance of the class. The class exposes methods, which define the interface to the object's state.
Consequences
  • Module structure: All data is directly accessible.
  • Closure structure: To be accessible, data must be explicitly exposed through closures.
  • Class structure: The language can provide privacy controls.
Implementation
The language support for these patterns varies considerably. Some languages support all versions, and some support none, while others are in-between. The module structure is possible to implement "by accident", and may be a candidate for refactoring into another model.
Sample Code
  • Module structure:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
"""An object-shaped module."""

_SECRET_LIST = []

def put_to_list(item):
    """A method-shaped function."""
    _SECRET_LIST.append(item)

def pop_from_list():
    """Another method-shaped function."""
    return _SECRET_LIST.pop(0)

# Use by importing the module.
  • Closure structure:
def constructor():
    """A constructor-shaped function."""
    a = 0

    def increment():
        """A method-shaped function. Increment a and return its value."""
        nonlocal a
        a += 1
        return a

    return increment

# Usage
increment1 = constructor()
increment2 = constructor()

assert increment1() == 1
assert increment1() == 2
assert increment2() == 1
  • Class structure:
class Class:
    """A class."""

    def __init__(self, secret):
        self._secret = secret

    @property
    def secret(self):
        """An accessor function/method."""
        return self._secret

assert Class(1).secret == 1
Known Uses
Like... any bit of Python code, technically? Rust does this in a statically-dispatched fashion.
Related Patterns
... All of them?

Where is the lie?

Anyway, point is, Homunculus's main loop should be an "update" function or several, a method on some kind of "Game" object.

Meanwhile, the "blog.py" script is really showing its roots. I need to divide the global variables up by problem domain, and make classes for stuff like Mercurial repositories, Pelican projects, Neocities sites, and LDoc.