Skip to content

LAMBDA idea: RECORD / WITH — closure-based state (research note) #121

@jimmytacks

Description

@jimmytacks

Origin

Synced from Notion: RECORD / WITH — closure-based state (research note)

Content

Status

Research note, not a competitive-use proposal. Documented for completeness as an alternative approach to multi-variable REDUCE state. Unlikely to be used in 30-minute cases — too much upfront structure for the time pressure — but the technique is interesting and may inform future Loop primitives.

Problem space

Same as the existing Loop primitives (SetVar / GetVar / NewState): how do you carry multiple state variables through a single-cell REDUCE accumulator? The SetVar/GetVar approach packs state into a delimited string. RECORD/WITH takes a fundamentally different approach — it represents state as a LAMBDA closure.

Source

Discovered in charter_v0.1.4_release.xlsx, Sheet1!A8 — a 4-round Warrior-vs-Dragon combat simulation built as a single-cell REDUCE.

How it works

A WITH(...) call returns a LAMBDA. Field access is a function call:

  • WITH("hp", 100, "atk", 25) returns a callable
  • _hero("hp") calls that LAMBDA with "hp" and gets back 100

The LAMBDA's body is effectively a switch on the first argument. Field names are baked in as string literals at definition time.

RECORD("fighter", WITH(...)) wraps a record with a type tag and adds an "update" sentinel:

  • _hero("update", WITH("hp", 50)) → returns a new record with merged fields
  • This is how field updates work in a system whose only operation is "call with a string"

Records nest natively: the REDUCE accumulator in the example is itself WITH("hero", _hero, "boss", _boss, "log", "") — a record holding two records and a string.

Comparison to SetVar / GetVar

Dimension SetVar / GetVar RECORD / WITH
Representation Delimited string LAMBDA closure
Read GetVar(s, "hp") → text _hero("hp") → native value
Write SetVar(s, "hp", 50) _hero("update", WITH("hp", 50))
Numeric coercion Required (--, +0) Native
Nested records Manual serialization First-class
Methods on records None Yes (e.g. _alive, _damage)
Per-step cost O(n) string parse O(1) closure call
Total loop cost O(n²) O(n)
Cell-size ceiling ~32K chars of state None — closures aren't serialized
Introspection SplitState renders to a row None — closures are opaque
Typo safety Any string is a valid key, missing key returns NA Silent fallback on unknown strings

Magic-string trade-off

RECORD/WITH leans on convention. Field names ("hp", "atk"), method names ("update"), and type tags ("fighter") are all string literals the implementation recognises. Typos won't error — they'll silently return the fallback branch. SetVar/GetVar avoids this because the operation is encoded in which LAMBDA you call, not in what string you pass.

Why not for competitive use

  • Requires defining the record schema (constructor LAMBDA + every method) before the loop runs. In a 5-minute level that's overhead the SetVar/GetVar generic store doesn't impose.
  • Magic-string typos fail silently — debugging eats clock time.
  • Benefit is mostly performance and nesting depth, neither of which matter for typical case-sized state.

Where it might inform future work

  • The "callable record" pattern could justify a RECORD primitive in the library if a case ever benefits from native nesting or O(1) reads.
  • The "update sentinel" trick is a neat workaround for the no-mutation constraint and worth remembering.
  • A debug/introspection helper for closure-records (the missing SplitState equivalent) would be needed before this is usable under time pressure.

Reference formula

The Warrior-vs-Dragon simulation from Sheet1!A8 of charter_v0.1.4_release.xlsx:

=LET(
  _fighter, LAMBDA(_n,_hp,_atk,_def,
    RECORD("fighter", WITH("name", _n, "hp", _hp, "atk", _atk, "def", _def))),

  _damage, LAMBDA(_a,_d, MAX(0, _a("atk") - _d("def"))),

  _hit, LAMBDA(_a,_d,
    _d("update", WITH("hp", MAX(0, _d("hp") - _damage(_a, _d))))),

  _alive, LAMBDA(_f, _f("hp") > 0),

  _hero, _fighter("Warrior", 100, 25, 10),
  _boss, _fighter("Dragon", 200, 35, 15),

  _final, REDUCE(WITH("hero", _hero, "boss", _boss, "log", ""),
    SEQUENCE(20), LAMBDA(_s,_rnd,
    IF(OR(NOT(_alive(_s("hero"))), NOT(_alive(_s("boss")))), _s,
    LET(
      _b, _hit(_s("hero"), _s("boss")),
      _h, IF(_alive(_b), _hit(_b, _s("hero")), _s("hero")),
      WITH("hero", _h, "boss", _b, "log", _s("log") &
        "R" & _rnd & ": " &
          _damage(_s("hero"), _s("boss")) & " dmg to " & _b("name") &
          " (" & _b("hp") & " HP)" &
        IF(_alive(_b),
          " | " & _damage(_b, _s("hero")) & " dmg to " & _h("name") &
          " (" & _h("hp") & " HP)",
          " — KO!") & CHAR(10)))))),

  VSTACK(
    TEXTSPLIT(_final("log"), , CHAR(10)),
    "",
    IF(_alive(_final("hero")),
      _final("hero")("name") & " wins with " & _final("hero")("hp") & " HP!",
      _final("boss")("name") & " wins with " & _final("boss")("hp") & " HP!")))

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or improvementlambda-ideaLAMBDA function idea for the backlogstatus: backlogNot yet started

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions