DEV Community

mayakashi
mayakashi

Posted on

X-MaP (Cross-Mapped Programming): Treating the Header Row as a Schema, Rows as Instances, and Columns as Lazy Properties

A Japanese-language version of this article is available on Zenn.

I've prepared a blank map.
Fill in what you need, as your own — and draw the shipping lanes, the pipelines, so that data, the traveler, can reach the right destination.

Introduction

There's an old, well-known technique: put a map in the header row and call into it from the middle of your code by key. This article generalizes that pattern into a full paradigm: the header row is a schema, each row is an instance, each column is a lazy property, and each cell is a polymorphic entity that can hold a value, a function, or a predicate expression. We call this paradigm X-MaP (Cross-Mapped Programming), positioned alongside object-oriented and functional programming as a language-agnostic design paradigm.

This article presents the paradigm's principles, not an implementation guide for any specific language or framework. Implementation details — dependency resolution order, caching strategy, cycle detection, and so on — are deliberately left to the developer to define for their own environment, and are not covered here.

Core Principles (7)

X-MaP consists of the following seven principles:

  1. The header row = a schema (a pseudo-metaclass). Each column declares the return type of its getter
  2. A row = an instance
  3. A row's identifier is an identifier group of 1 to N columns (never fixed to a single id column)
  4. A column = a lazy property (lazy getter)
  5. A cell = a polymorphic entity that can be a value, a function, or a predicate expression
  6. Normalization (3NF-equivalent) = the principle for dividing responsibility across multiple dbs
  7. Cell access goes through a single function, lookup, as its only path. At its fullest, this function can take four kinds of information: which db (table) to look in, which row (identifier group), which column (lazy getter key), and any extra arguments needed to resolve the value (e.g., lookup(table, identifiers, key, args) — this ordering is one example, not fixed)

On Principle 3: why an identifier must never be a single column

The essence of this principle is that it achieves structural typing and human cognition at the same time, as a single outcome of a single choice. A single id column, however unique its value, is an opaque atom with no internal structure — its meaning is unreadable to a machine and a human alike. Split the identifier into a group of 1 to N columns, each carrying its own meaningful dimension (its own type), and the identifier itself becomes structured data.

From the machine's side, this is structural typing: the shape of the identifier itself becomes something that can be validated, matched, and pattern-matched against. From the human's side, it's the same thing seen differently: the class (row) reveals, directly in its identifier values, how it came to be — what chain of conditions produced it. This is the same information a human used to read by tracing a nested if in code to see why execution ended up on a given branch.

The point is that this isn't "a benefit for machines" and "a benefit for humans" existing as two separate things — it's one and the same choice, structuring the identifier, that serves both simultaneously. That's precisely why this is treated as a principle rather than a mere recommendation.

As a secondary benefit: when the number of rows (classes) grows very large, having identifiers spread across multiple columns lets you narrow down target rows by combining conditions, the way a spreadsheet filter works. This makes the scope of a bulk edit or audit explicit, which reduces the chance of missing a row that should have been updated.

On Principle 6: normalization is about the boundary between dbs, not within one

3NF is traditionally used to decide how to split rows within a single table. Here, it's repurposed as the criterion for how to divide responsibility across multiple dbs: any attribute group found to have a transitive functional dependency should be split out into its own db.

This is a completely separate axis from column-wise splitting, discussed next.

On Principle 7: why route all cell access through a single lookup function

lookup can take four kinds of information at its fullest (one example: lookup(table, identifiers, key, args) — this particular ordering isn't something you need to follow).

  • table: which db (table) to look in. Since Principle 6 assumes multiple dbs can exist side by side, lookup in its general form needs to know which one it's addressing
  • identifiers: which row. This reflects the identifier group from Principle 3 (spanning 1 to N columns) directly in the signature. A single-key setup just passes a single-element array or tuple; the signature never has to change for the composite-key case
  • key: which column (lazy getter key)
  • args: any extra arguments a cell's function/predicate needs when it can't derive everything from the row's own values alone

This is lookup's general form, and the principle itself is defined at this full extent.

When passing a lookup to a leaf (a terminal processing unit in a pipeline), table and part of args are usually already fixed, so what actually gets passed is a closure with those partially applied — reducing to the minimal lookup(identifiers, key) form. Passing a row object or the entire table directly into a leaf would leak the underlying storage format (md, csv, RDB, in-memory dict) into the leaf; passing only this minimal closure instead means:

  • The storage format stays hidden inside the lookup function
  • The leaf never needs to know the table exists at all

In other words, the minimal form isn't the principle itself — it's a special case of the full form, with table and args already bound in advance.

On Column-Wise Splitting (Cognitive Load, Not a Principle)

As a table grows wider, moving further to the right increases eye movement and the likelihood of misreading which column corresponds to which value. This is a pure UI/cognitive-ergonomics problem, unrelated to 3NF.

There's a second, separate problem when md/csv files like these are managed under git: most diff tools are built around line-level change detection, and an extremely long line makes it harder to see exactly what changed, which in turn makes review harder — which column actually moved is not obvious at a glance. Column-wise splitting is therefore not just a readability measure for humans; it's also a practical necessity for keeping reviews tractable under version control.

X-MaP states only that column-wise splitting exists as a necessity when cognitive load becomes a problem; the specific threshold and splitting strategy (a hard column-count limit, grouping by access frequency, splitting by semantic cluster, etc.) are left unspecified, for developers to define per their environment. This mirrors how OOP states only the principles of encapsulation and polymorphism, leaving concrete class-design guidelines (e.g., SOLID) to a separate layer.

Recommendations (Non-Normative, Environment-Dependent)

The following are recommendations, not principles, and are not mandatory.

  • Storage format: the essential point is that the editing environment behaves like a spreadsheet — not the underlying storage format itself. Whether you choose md tables, csv, spreadsheets, XML, or JSON is purely a means to an end; any format is fine as long as the editing experience lets you see each row as a record, sort and filter by column, and bulk-edit column-wise. Even a format that's poor for at-a-glance reading as raw text — XML or JSON, for instance — is acceptable if a tool (a VS Code extension, for example) presents it as a grid/table view for editing. Conversely, choosing md/csv as the storage format alone doesn't satisfy this recommendation if the editing environment doesn't actually treat it like a spreadsheet
  • Template literals in code: in environments where real file I/O is constrained or costly — Google Apps Script (GAS) being a typical example — it's fine to embed md-equivalent text as a template literal directly in code and parse it at runtime instead of using an actual file. Think of it as separating "md as notation" from "md as a file": Principles 1–7 don't depend on where the text physically lives, so this substitution doesn't compromise the paradigm. When hardcoded as a template literal, edits are better made by copying the string out into a proper file of the matching notation (md/csv/etc.), editing it there where syntax highlighting and table-alignment tooling can catch mistakes, and pasting it back — rather than editing the string literal in place, where a misaligned column can go unnoticed by the editor
  • Avoid empty cells: when a column is added, this forces every existing row to explicitly declare a value, which prevents implementation gaps from silently persisting. Whether this constraint can be machine-verified by the format itself depends on which storage format you choose. Formats with mature schema-validation mechanisms — XSD or RelaxNG for XML, JSON Schema for JSON — let you enforce a rule like "no empty cells" declaratively, via minOccurs or a required-property list, with no need to write a custom linter. Plain md/csv has no such mechanism, so enforcing the same constraint means building your own validation tooling. This is a separate axis from the editing-experience question above — it's worth evaluating storage formats on "can constraints be machine-verified" as its own criterion
  • Error handling on lookup failure: dumping the call arguments (or otherwise preserving context) at the point of failure keeps pipeline design simpler
  • A column describing the class: a self-documenting column explaining what each row (instance) represents is desirable
  • Load and cache the db at startup: if the file is re-read on every lookup call, using a real file format (md/csv, etc.) means paying I/O cost on every single access. It's better to load the table once at startup, keep it in memory, and have lookup read from that cache. This is distinct from how you memoize the computed result of each lazily-evaluated cell (left as an unspecified corollary) — this recommendation is only about keeping the cost of loading the table itself to a single pass at startup

Benefits

1. The db becomes part of the specification

Because the header declares each getter's return type, the schema table doubles as a type-signature table. Since the specification and the implementation live in the same file, drift between spec and implementation is structurally discouraged.

2. Solves management problems inherent to OOP and FP

2-1. Makes implicit dependencies visible

In OOP, state tends to be implicitly scattered across classes, making dependencies hard to see. In FP, the type signatures of composed functions often aren't visible until runtime. X-MaP forces both onto a single visible surface — the table — so implicit dependencies are structurally exposed.

2-2. Structurally prevents implementation gaps

This follows directly from the "avoid empty cells" recommendation. Adding a column (a feature) forces every existing row (a class) to declare a value. This mechanically prevents the kind of gap that occurs in OOP when a new method is added and a subclass silently fails to override it — the table's visibility alone catches it.

3. Reduced branching frequency → theoretical speed gains

Replacing branches with dispatch-table lookups can convert the cost of CPU branch mispredictions (and the resulting pipeline flush) into the cost of a table lookup instead. As of this writing, no benchmarks have been run — this remains a theoretical expectation, not a measured result.

4. Decoupled leaves enable separated testing responsibility

As long as a leaf satisfies its contract — "given input, return the type declared in the schema" — the pipeline only needs to verify that the wiring is correct. This structurally separates contract testing (schema conformance) from wiring testing (pipeline design). The identifiers-bound lookup closure described above strengthens this decoupling further.

5. Ease of extension

To add... Do this
A class (instance type) Add a row
A feature (property) Add a column
The implementation Fill in the intersecting cells

Because empty cells are disallowed by recommendation, adding a column mechanically forces implementation across every existing row, so extension never leaves gaps.

6. Fewer concurrent-editing conflicts

Adding a row or a column is each an independent unit of change. When multiple people edit different rows or columns at the same time, merge conflicts are unlikely unless the changes actually intersect. This gives a structurally smaller conflict surface than OOP, where several developers touching different parts of the same class is a common source of merge friction.

As an aside: because rows are enumerable instances and each cell's return type is declared in the schema, the same table doubles fairly naturally as input data for table-driven tests.

Scope of Applicability: Use Alongside OOP/FP

In tight hot paths — a simple while loop doing sequential computation, for example — the overhead of a function call plus a type check on every cell resolution can outweigh the cost of a plain if branch.

For this reason, X-MaP is best understood not as a replacement for OOP/FP but as a design principle applied to the layer defining relationships between entities, branching logic, and business rules. The practical approach is to keep hot-path computation in OOP/FP as usual, and use X-MaP to loosely couple the pieces around it.

Adoption Path When Developing with AI

When development is done by having an LLM write the code, AI as of this writing hasn't seen X-MaP in its training data. Simply instructing it to "write this in X-MaP" tends to produce output that ignores the principles inconsistently.

A more realistic adoption path is to first have the AI implement things the conventional way (OOP/FP style, nested if statements included), then have it refactor that into X-MaP's structure with this article as a reference. The correspondence between nested if branches and identifier groups / predicate cells (see the note under Principle 3) fits naturally with this staged refactoring approach.

As AI models pick up enough X-MaP in their training data, this two-stage transitional workflow should become unnecessary.

Left Unspecified (Corollaries, Left to the Developer)

The following are deliberately excluded from the X-MaP principles and left for implementers to resolve per their environment:

  • Dependency resolution order between cells, and cycle detection
  • Caching strategy for lazily-evaluated results
  • The concrete design of the boundary between X-MaP and hot-path code

On the Name

Cross-Mapped Programming (X-MaP). The choice of "Mapped" — a past-participle adjective — was made so that it sits grammatically parallel to "Oriented" in Object-Oriented Programming. The abbreviation X-MaP is used as an informal short form throughout.

Summary

X-MaP is a language-agnostic design paradigm built on the structure "header = schema, row = instance, column = lazy property, cell = polymorphic getter entity," with 3NF repurposed as the criterion for dividing responsibility across dbs, and a single lookup function as the sole path for cell access. Implementation details such as concrete splitting strategies and caching are deliberately left unspecified, presenting X-MaP as a set of principles that can stand alongside OOP and FP rather than replace them.


A Disclaimer

If reading this article leaves you feeling lost about what to do with it, that probably just means it's not the right time to use it yet.

What I've handed over here is nothing more than a blank map. Fill it in with whatever you need for your own journey (your own development work). Some people will draw in the terrain; others will mark the spots worth visiting along the way. Both are valid uses of the same map. And if you can't think of anything to add to it, that probably just means you haven't yet run into the situation where this map would help.

Top comments (0)