From Programming Paradigms to Metaphysics

How do we model potency in software?

Every time we define a class, construct a type system, design a database schema, or decide how behaviour should attach to data, we make claims about what exists in the universe of our application. We decide what kinds of things there are, what makes one thing different from another, which properties belong to things necessarily, which can change, and what those things are capable of doing.

When we model a domain, we abstract from reality and reconstruct those abstractions within a formal system. Paradigms provide different conceptual structures through which that representation can be organised.

Different paradigms make different aspects of reality natural to express. Some emphasise stable kinds and explicitly defined structures; others emphasise behaviour and changing capabilities; still others separate identity, state, and the operations acting upon them. This article considers three broad approaches.

Static Composition

A statically typed language such as C# encourages us to define things according to stable types whose permissible structures and behaviours are known in advance. Compile-time metaprogramming techniques such as macros and templates can generate or compose additional structure, but that structure is still determined before runtime; some forms of AOP operate similarly through compile-time weaving.

Dynamic Composition

A dynamic object-oriented language such as Ruby allows an object's capabilities to change at runtime, even when those capabilities were not anticipated by its original type.

Data-Oriented Composition

And a data-oriented architecture such as Entity-Component-System (ECS), with architectural affinities to ideas familiar from functional programming, separates identity, state and behaviour almost entirely: entities become thin identities, components describe their current properties and dispositions, and external systems operate upon entities possessing the relevant data.

Each approach captures certain aspects of reality extremely well while making others awkward to express.

And the differences between them become particularly interesting when viewed through concepts developed more than two thousand years before the first computer: substance and accident, essence and property, potency and act, and acquired habit.


1. Types, Universals, and the Stability of Essence

An ancient dispute over universals is traditionally illustrated by an objection attributed to Antisthenes:

"I see a horse, but I do not see horseness."

The point of the objection is immediately recognisable to a programmer. We encounter individual things—a particular horse, a particular person, a particular invoice—but our intellect identifies something common by which those particulars can be understood as instances of a kind.

In a statically typed object-oriented language, we might write:

public class Human
{
    public string Name { get; set; }

    public void Walk()
    {
        // ...
    }
}

Human describes something universal. John, by contrast, is one concrete instance of it:

var john = new Human();
john.Name = "John";

There is another interesting parallel here with Aristotelian-Thomistic metaphysics. We understand both John and Peter as human because they share the same nature—humanity—yet John is not Peter.

For Aristotle, form is the principle by which a material thing is the kind of thing it is. In Aquinas, designated matter (materia signata) accounts for its individuation—why John is this particular human rather than Peter.

The analogy with programming is interesting. A type or class plays something like the role of form within our computational model: Human expresses what our software understands a human to be, while john is a particular instantiation of that definition. Instantiation provides individuation within the computational model.

The compiler knows what a Human is. It knows which properties humans possess, which operations are defined upon them, and which relationships between types are legitimate.

An Invoice, Currency, PostalAddress, or LegalContract may have important constraints. The rigidity of the type system allows the programmer to assert propositions about the domain, and the compiler can enforce them.

A difficulty appears when we confuse what something is with everything it might become capable of doing. Our concern here is specifically with dispositions and potencies that give rise to capabilities and operations, rather than mutable properties such as hair colour.

Here we encounter a tension between type and object. The type describes the nature of the thing, but a particular object may actualise potencies and acquire dispositions that do not belong to that nature as such.

A related case is state transition, which can represent the actualisation of a possibility. In the classic State pattern, the possible behaviours are generally established structurally in advance, while at runtime the object changes which state governs its behaviour:

John
  └── CurrentState → StateA
                   → StateB
                   → StateC

State therefore has an interesting relationship to potency and act, but represents a different concern from the acquisition of new dispositions explored here. A similar observation applies to runtime AOP, where behaviour may be selected or introduced through interception without structurally modifying the object.

A human being can become a programmer, boxer, singer, carpenter, mathematician, chess player, or pianist. Yet none of these things appears to belong to the definition of humanity itself.

If we attempt to express all of these possibilities through a conventional class hierarchy, we encounter trouble.

Should we write:

Mathematician : Human
Boxer         : Human
Singer        : Human

What happens when the same human is both a mathematician and a boxer?

And what happens when someone learns mathematics at thirty years old?

The person's identity has persisted. Something about them has changed, but they have not become a different kind of substance in the same sense that distinguishes a human from a horse.

This is where the Aristotelian distinction between potency and act becomes useful.


2. Aristotle, Aquinas, and Acquired Habit

Aristotle's account of change distinguishes between what something presently is and what it has the potency to become or do. A seed is not presently a mature tree, but possesses a potency toward becoming one. A person who has never studied algebra does not presently possess mathematical knowledge, but may possess the capacity to acquire it.

Thomas Aquinas develops this Aristotelian framework through distinctions between substance and accident, and through the particularly important notion of habit (habitus).

A substance exists in itself. Accidents exist in a substance and modify it without necessarily changing what kind of thing it fundamentally is. Among these accidental qualities, a habit represents a relatively stable disposition. Aquinas describes habit as a quality or disposition with a certain lastingness and an ordering toward nature or operation. Acquired knowledge provides a particularly useful example.

Suppose John studies mathematics.

Before studying algebra, John has the underlying human capacity to learn it. After studying and practising, something real has changed in John. He has acquired knowledge and therefore acquired a new capacity for action. He can now solve equations that he previously could not.

His underlying humanity has not been replaced. Yet his actual dispositions and capabilities have changed.

This distinction creates an interesting problem for software modelling.

If Human contains every possible behaviour, the class becomes an inventory of human potentiality:

class Human
{
    void Walk();
    void Sing();
    void Box();
    void Program();
    void SolveEquation();
    void PlayPiano();
    // ...
}

John can now apparently solve equations simply because he is human.

The software has confused potency with actuality.

Inheritance does not entirely solve the problem either.

Mathematician : Human models mathematician as a kind of human. But being a mathematician is not a different kind of human in the same sense that a taxonomic subtype represents a different kind of thing. It is something a human can become through acquiring knowledge.

Interface composition encounters the same problem. A Human can implement several capability contracts—IProgrammer, IBoxer, IMathematician—but if every possible habit is represented by another interface, we risk interface explosion. More fundamentally, those capabilities remain part of the static definition of the type.

But being a mathematician is acquired. John can become one while remaining a human. Peter may not know mathematics, but he remains equally human.

A type represents what a thing is, but a particular thing can actualise potencies and acquire dispositions without ceasing to be what it is. How should software represent that?

This is precisely the sort of problem that dynamic object systems handle remarkably naturally.


3. Ruby and the Acquisition of Capability

We might represent mathematical capability as a module:

module Algebra
  def apply_equations
    # ...
  end
end

John can initially exist without it:

john = Human.new

Later, he acquires the capability:

john.extend(Algebra)

john.apply_equations

Something significant has happened. John remains a human. His original identity has not been replaced, and we did not need to anticipate Algebra when defining Human.

Yet John can now perform an operation that he previously could not.

Ruby's extend mechanism specifically adds a module's functionality to the receiving object, rather than requiring the module to be included in the object's class.

As a computational analogy, this maps surprisingly well onto the notion of an acquired habit: an existing subject acquires a disposition that gives rise to capabilities which were not previously actualised in it.

This flexibility is one of the attractions of dynamic languages.

Instead of demanding that the programmer determine the entire taxonomic structure of reality before execution, the runtime itself can participate in the composition of objects.

We suddenly encounter a beautiful world of living, breathing objects.

But that freedom has a price.

If John can acquire Algebra, what prevents a BankAccount, HTTPConnection, or Potato from acquiring it?

Dynamic languages can therefore express changing capabilities extremely naturally, but the same mechanism that gives them their flexibility can weaken the structural guarantees available to us. We potentially lose some of our ability to assert domain predicates and have those predicates enforced by the type system, and with it some of the predictability that such constraints provide. The program may permit combinations that make no sense within the ontology of the domain.

We have gained expressive freedom, but surrendered some constraint.

It is worth noting that prototype-based languages such as JavaScript also provide dynamic object models, allowing individual objects to acquire properties and behaviour at runtime:

const john = {
    name: "John"
};

john.solveEquation = function () {
    // ...
};

4. A Third Model: Separating Identity, Disposition, and Action

Modern game development faced an unusually difficult modelling problem. A game world can contain thousands or millions of things whose capabilities overlap in unpredictable ways.

  • A wooden door can burn.
  • A tree can burn.
  • A shield can burn.
  • Perhaps a character's clothes can burn.

Yet these things do not belong to an inheritance hierarchy merely because they share the disposition of flammability.

At the same time, games care intensely about performance. The unconstrained runtime flexibility of a highly dynamic object model is often undesirable when enormous numbers of entities must be processed every frame.

One influential response is the Entity-Component-System, or ECS, architecture.

ECS separates three things that conventional object-oriented design tends to combine:

ENTITY
    identity

COMPONENTS
    data, properties, state, dispositions

SYSTEMS
    operations over entities possessing relevant components

This produces a radically different model of the software universe.

An entity may be little more than an identifier:

Entity 2048

By itself, that tells us almost nothing about what entity 2048 is.

Instead, the entity participates in the world through its associated components:

Position
Velocity
Health
Flammable

And behaviour is moved outside the entity into systems:

MovementSystem
DamageSystem
CombustionSystem

The result has interesting parallels with Aristotelian and Thomistic ontology.


5. Flammability, Potency, and Efficient Cause

Consider a piece of wood.

The fact that it is not currently burning does not mean that burning is impossible for it. Given the relevant characteristics and circumstances, it possesses a disposition toward combustion.

We might describe it as flammable.

In an ECS design, this could be represented explicitly:

struct Flammable
{
    public float IgnitionTemperature;
}

The component contains no Burn() method.

It represents data describing a disposition.

A completely separate system can then operate upon things possessing the relevant disposition:

CombustionSystem:

    find entities with:
        Flammable

    if Temperature >= IgnitionTemperature:
        initiate combustion

The system does not need to ask whether the entity is a Tree, Chair, House, or Shield.

Those classifications may matter elsewhere, but they are not what matters to combustion.

For this particular operation, what matters is whether the relevant potency or disposition is present and whether the conditions required for its actualisation obtain.

The component represents something about what the entity is presently disposed toward; the system supplies an external operation through which that potential can be brought into act.

Instead of asking:

"What class of thing is allowed to burn?"

we ask:

"Which things presently possess the characteristics required for combustion?"


6. The Functional Influence: Behaviour Outside the Object

ECS also reflects a broader movement away from the classical object-oriented idea that data and behaviour must always reside together. Traditional object-oriented programming often encourages:

object.doSomething()

The object's data and the operations acting upon it are conceptually bundled together.

Many functional programming styles encourage a different perspective. Data can be represented explicitly, while transformations over that data are expressed separately. Conceptually:

newState = operation(oldState)

ECS is not synonymous with functional programming, and many ECS implementations are not purely functional. Nevertheless, the architectural affinity is important:

  • Components tend toward data.
  • Systems tend toward transformations and operations.
  • Entities provide identity and association.

This gives statically typed languages an interesting middle ground.

A Flammable component can itself be completely statically typed. Its structure is known at compile time. A Position component can be equally rigorous.

What changes dynamically is not necessarily the definition of those components, but their composition.

  • Entity 2048 can acquire Flammable.
  • Entity 8923 can lose Movable.
  • Entity 7034 can acquire AlgebraKnowledge.

The building blocks remain statically defined while their assemblage changes during execution.

We have therefore recovered some of the dynamic compositional power seen in Ruby without necessarily making the individual structural elements themselves dynamic.

But once again, this is not free.


7. The Price of Thin Entities

In conventional object-oriented code, this declaration tells us something substantial:

Human john;

Whatever else may be true about John, the type system tells us that John is a Human.

A pure ECS architecture may instead give us:

Entity 2048

The identity itself says almost nothing.

To discover what the entity currently represents, we must inspect its components.

The entity has become ontologically thin.

This can be enormously useful in domains dominated by composition, simulation and changing state. But in domains where stable identity and invariant classification matter greatly, it may obscure information that a richer type system would make immediately explicit.

ECS can also introduce its own complexity. Behaviour that was once discoverable on an object may now be distributed across systems. Relationships between components can require explicit invariants. Invalid component combinations remain possible unless the architecture provides mechanisms to prevent them.


8. Three Ways of Representing a Domain

We can now see the three approaches as different ways of representing a domain.

Static modelling

Its great strength is explicit nature and constraint.

Types allow us to state what something is and to assert certain propositions about it.

Its weakness appears when what belongs to a particular object contingently—acquired or changing capabilities—is forced into the static definition of its type.

This approach is well suited to domains where stable identity, explicit invariants, and well-defined business states and behaviours are important. Many enterprise domains fit this model naturally: customers, invoices, orders, accounts, and transactions often benefit from making their structure and permissible operations explicit in the type system.

However, there is more to the story. We don't have to represent the acquired disposition structurally.

A mixed approach can recover flexibility.

A statically typed Human might retain its explicit type and identity while aggregating behaviours via objects, delegates or functions at runtime. This allows particular objects to acquire capabilities without changing their underlying type, although those capabilities must then be discovered or invoked through some additional mechanism, such as interfaces, runtime type tests, or explicit lookup.

// acquire behaviour
john.Behaviours.Add(new Algebra());

// then later
var algebra = john.Behaviours.OfType<IAlgebra>().FirstOrDefault();

algebra?.ApplyEquations();

Another common approach is to use a generic, command-like protocol:

public interface IBehaviour 
{
    void Execute();    
}

Or, in games:

public interface IBehaviour
{
    void Update(float deltaTime);
}

The owning object can then simply iterate over its attached behaviours:

foreach (var behaviour in john.Behaviours)
{
    behaviour.Update(deltaTime);
}

This avoids explicit capability lookup entirely.

The trade-off is that everything has been collapsed into a common behavioural protocol: the semantic meaning of the individual capability is no longer expressed by the invocation itself. This is not inevitable, however. Late-bound mechanisms can preserve semantically meaningful invocation while resolving the underlying behaviour at runtime. In C#, for example, the Dynamic Language Runtime can intercept a named operation and resolve it dynamically; Delphi can provide similar late-bound invocation through IDispatch.

Related variations include object-component and object-node approaches, which retain a richer object identity while composing functionality through attached components or nodes.

Another useful approach to composing additional behaviour is the Decorator pattern, in which an existing object is wrapped with additional capabilities:

Human
  ↓
MathematicsDecorator
  ↓
BoxingDecorator
  ↓
ProgrammingDecorator

These decorators can be composed at runtime, extending the object's behaviour without modifying its type. These are not structural changes; nevertheless, we can see that a static type system has options.

Roles provide another mechanism for dynamically composing behaviour around a stable underlying identity.

Dynamic modelling

Its great strength is runtime acquisition and composition.

Unlike the mixed static approach, this acquisition can be structural rather than merely delegated: the particular object itself can acquire methods and capabilities that were absent from its original type and from other instances of that type.

john.extend(Algebra)
john.apply_equations

Objects can therefore gain behaviours and dispositions without changing their underlying identity or requiring every possibility to have been anticipated in their original definition.

Its weakness is that flexibility can exceed the constraints of the domain. If almost anything can be attached to almost anything, ontologically nonsensical states may remain computationally legal.

This approach suits domains where capabilities are fluid, composition is highly contextual, or objects must evolve in ways that are difficult to anticipate in advance. Extensible applications, scripting environments, metaprogramming-heavy systems, and domains whose rules change dynamically can benefit from allowing behaviour to be composed at runtime.

ECS and data-oriented modelling

Its strength is the separation of identity, disposition and operation.

Strongly defined components can be composed dynamically, while external systems act upon entities possessing the data relevant to their operation.

Its weakness is almost the inverse of classical object orientation: the entity itself can become so thin that the software loses an immediately visible account of what the thing actually is.

This approach favours domains where large numbers of entities share overlapping dispositions and capabilities, and where runtime composition is important. Simulations and games are obvious examples, but the same model can be useful wherever systems operate over heterogeneous collections of things according to the properties they presently possess.

Summary

We can summarise the approaches according to where the realised capability is represented:

Static composition: Human defines the capability as part of its type.

Mixed static composition: John contains an Algebra behaviour.

Dynamic composition: John acquires algebraic methods as part of his effective runtime structure.

ECS: John possesses data representing the relevant disposition, upon which an external system can operate.


Conclusion: The Software Architect as Applied Ontologist

How do we model potency in software?

The answer lies in considering which paradigm best represents the domain before us, given our constraints:

  • What kinds of things exist within it?
  • What belongs to their nature, and what is contingent?
  • Which invariants must the type system enforce?
  • What can change while a thing remains the same thing?
  • What dispositions can it acquire or lose?
  • Must those capabilities be known at compile time, or can they be composed at runtime?
  • What is merely possible, and what is presently actual?
  • Where should behaviour reside: on the object, in an acquired behaviour, or in an external system?
  • What operations follow from the properties or dispositions it possesses?
  • And which of these distinctions must the software itself preserve and enforce?

This is where Aristotle and Aquinas remain unexpectedly useful to the software architect. Their metaphysics provides a vocabulary for thinking more precisely about being, identity, nature, property, change, potency, habit and action—the very things we repeatedly attempt to represent when constructing software systems.

Programming paradigms give us different tools with which to express those distinctions.

The software architect is, whether consciously or not, an applied ontologist.