Understanding Composition in Programming: Importance, Support in Argentum, and Differences from Mainstream Languages

What is Composition

Composition is a relation between objects when object A explicitly and exclusively owns the object B. Or in other words "The object B has only one owner - the object A".

Compositions is a bread and butter of all application data models:

  • Each HTML DOM element has exactly one parent element.
  • Every paragraph belongs to a specific text flow in an office document.
  • Each operator in the compiler AST belongs to exactly one expression.
  • Each game engine actor belongs to exactly one scene.
  • Each database record resides in one specific table.
  • Each control exists in exactly one GUI form.

All this examples illustrate the golden rule of composition:

No matter how complex is the overall connection graph of the application mutable data model,
its ownership subgraph is always a tree.

If a mutable object is shared across different hierarchies, it can result in catastrophic consequences:

  • Accidental changes to one record in a table can cause unintended changes to random records in other tables.
  • If the button pressed state is changed in one form, it may cause unintended changes to random buttons in other forms or windows.
  • Edits to a paragraph in an office document can cause accidental unintended changes in random places within the same or other documents.

Mutable sharing may be desirable in certain scenarios, but it should be done carefully:

  • it should be explicit,
  • visible for user,
  • it should be performed in a controlled manned allowing to break this sharing link,
  • multiple issues must be considered and resolved: like what to do if original or referencing object is deleted, moved, copied, stored in file, passed to another host, if shared part in one document turns into a cross-document and even a cross-domain sharing.

The implementation of mutable sharing requires significant investment in planning, UX/UI research, design, and thorough testing. However, many developers tend to overlook these complexities, which can result in errors and bad experiences for end-users. For example, Google Drive's data model was recently redesigned from multi-parenting to shortcuts due to the challenges associated with sharing files among multiple folders. In the new model, each file resides in a single folder, and other folders/drives store a non-owning link to the original file. This redesign followed the golden composition rule mentioned earlier.

Shallow vs Deep copy

Imaging a Building that is composed of Walls and a Roof:

class Building {
   walls = Array(Wall);
   roof = Roof;
   ...  // lots of methods were hidden here
}
myHouse = Building
  .addWall(Wall....)
  .addWall(Wall....)
  .addRoof(FlatRoof);
classDiagram myHouse_var *-- Building Building *-- Roof Building *-- Array Array *-- Wall1 Array *-- Wall2

Imagine that we need a mutable copy of this myHouse object:

houseCopy = @myHouse

Does it make sense to make a shallow copy of the Building?

classDiagram myHouse_var *-- Building Building *-- Roof Building *-- Array Array *-- Wall1 Array *-- Wall2 copyHouseVar *-- BuildingCopy BuildingCopy *-- Roof BuildingCopy *-- Array

It doesn't. It breaks the main composition rule: the Roof instance is now shared by both the original Building and its copy, which is not allowed in composition.

This makes the deep copy the only allowed copy operation type for mutable hierarchies.

classDiagram myHouse_var *-- Building Building *-- Roof Building *-- Array Array *-- Wall1 Array *-- Wall2 copyHouseVar *-- BuildingCopy BuildingCopy *-- RoofCopy BuildingCopy *-- ArrayCopy ArrayCopy *-- Wall1Copy ArrayCopy *-- Wall2Copy

How languages should support composition

To be Composition-aware, the programming language must:

  • include composition pointer type,
  • distinguish objects that are already owned from object that are not owned yet,
  • prevent the assignment of already owned objects to the composition pointers,
  • automate disposal of the object been pointed:
    • on new value assignment
    • on pointer destruction.

Although not mandatory, a good programming language should also:

  • automate making the deep copy,
  • allow for the rearrangement of nodes in a composition tree while taking measures to prevent loops and other violations of composition invariants.

How Argentum supports Composition

The simplest form of Argentum pointer field is a composition pointer:

class Wheel {....}
class Car {
   frontLeftWheel = Wheel;  // frontLeftWheel is a composition pointer to a wheel
}

a = Car;
a.frontLeftWheel := Wheel; // OK: assign a new instance to the field;

b = Car;
a.frontLeftWheel := b.frontLeftWheel;  // Error:
//  The `b.frontLeftWheel` instance is already owned by car `b`.

The @-operator automatically creates deep copies of objects:

a.frontLeftWheel := @b.frontLeftWheel;  // OK:
//  @ - is a deep copy operation

Argentum includes a splice operator that enables the reorganization of object hierarchies without the need for copy-destruction and with protection against loops. See more information about this operator in this post: parent-pointers-and-safe-n-fast-object-reattachments.

How other languages support composition

All GC-based and ref-counted based languages don't support composition. Programmers have to manually:

  • define copy operators,
  • keep an eye on all assignments to fields preventing loops and multi-parenting in object hierarchies
  • manually free-up resources used by no-more needed object, because it is not guaranteed when (if at all) these objects will be destroyed.

C++ provides some rudimentary composition features through std::unique_ptr. It automates subtree destruction and limits assignments. However, as it assigns by move, it is not protected from moving a pointer to an object to its own field, which can have negative consequences for both the application's business logic and memory management. Additionally, C++ does not provide a way for deep or shallow polymorphic copying. Instead, it automates object slicing, which is an error-prone practice that should be avoided whenever possible.

Rust has a Box pointer, which can simulate some of the behavior of a Composition pointer with the Clone/Copy traits. However, manual implementation is required for the Clone trait, and it is not compatible with objects that have resources (Drop trait)..

Conclusion

  • Composition is not primarily concerned with memory management, but rather with ensuring the sanity of application design and architecture.
  • Composition is a fundamental aspect of the data models used in modern applications..
  • Mainstream programming languages either do not support composition at all or have limited and inconvenient support for it.
  • Argentum programming language natively supports composition in a straightforward and effortless way.

Leave a Reply

Your email address will not be published. Required fields are marked *