The one seam, shown: Inline up close

작성자

카테고리:

← 피드로
DEV Community · Ernesto Herrera Salinas · 2026-07-30 개발(SW)

In post 10 I closed the composition-versus-coherence question with one paragraph: I
kept strict object-scoping, reserved an Inline operator for later, and rejected
automatic reach-down. That was true, and it was too fast. A reader told me the
reserved operator was not clear from a sentence, which is fair. A design record that
asserts a decision without showing it is not really a record. So here is the seam,
worked out.

First, the good news that made this only one seam and not ten: composition and
coherence mostly do not collide. Composition substitutes complex types; coherence
binds scalar facets; those are disjoint kinds of member. A collection gives one
persona per element. Draw order falls out of the eager construction rule from post 7.
And the resolver pipeline I pre-paid for back in post 4 turned out to be coherence’s
host. The two threads layer cleanly almost everywhere. Almost.

The discontinuity

Coherence is object-scoped (post 5). That one rule has a consequence that only shows
up once you have composition encouraging you to split a type across nested objects:
moving a facet into a child changes whether it coheres.

// Flat: Email is a Person facet, so it coheres with the name.
Customer { FirstName, LastName, Email }
//   -> "Maria", "Gonzalez", "maria.gonzalez@..."

// Decomposed: Contact is its own scope. A lone Email there does not activate a
// persona (one corroborating member, no name anchor), so it is a plain, unrelated email.
Customer { FirstName, LastName, Contact : ContactInfo { Email } }
//   -> "Maria", "Gonzalez", "rwilson@..."

Enter fullscreen mode Exit fullscreen mode

Same three fields, same intent, different result, decided entirely by which object
they live on. That is the discontinuity.

The options

A, strict: object-scoping stays. The decomposed email does not cohere.
   Maximally predictable. The gap is the already-deferred cross-entity work.

B, reach-down: a child with no entity of its own is absorbed into the parent scope.
   The email coheres. But "absorbed or not" now depends on hidden conditions.

C, Inline: an explicit operator that declares a child shares the parent's scope.
   Coheres when you say so, stays strict otherwise.

Enter fullscreen mode Exit fullscreen mode

I kept A as the v1.1 default and reserved C. The rest of this post is C, shown,
because a reserved operator you cannot picture is not a real reservation.

Inline, in three scenarios

Inline declares that a nested child shares its parent’s scope: its scalar facets
lift into the parent’s bundle. The child is still a real object; only its coherence
scoping changes.

Scenario one, the plain case. It pulls a value-object’s fields into the persona:

Lie.Define<Customer>()
    .Inline(c => c.Contact)        // Contact.Email/Phone join Customer's persona
    .Build();
//   FirstName="Maria"  LastName="Gonzalez"
//   Contact.Email="maria.gonzalez@..."   Contact.Phone= the Person's phone

Enter fullscreen mode Exit fullscreen mode

Scenario two, where it changes activation, not just correlation. A parent with only
an Email and a nested { First, Last } activate nothing on their own; inlined, they
clear the gate together and become one person:

// Account { Email, Name : NameParts { First, Last } }
.Inline(a => a.Name)
//   Name.First="David"  Name.Last="Okafor"
//   Email="david.okafor@..."   <- now the same Person; without Inline it was unrelated

Enter fullscreen mode Exit fullscreen mode

Scenario three, the one that turns a silent wrong-look into a fix. Decompose an
address across two blocks and each block coheres into a different real place:

// Office { City, State, Postal : PostalBlock { PostalCode, Country } }
// default:  City/State = Austin, Texas   and   PostalCode/Country = a German ZIP + Germany
.Inline(o => o.Postal)
//   one real row projects across both: Austin, Texas, 78701, United States

Enter fullscreen mode Exit fullscreen mode

Each block was internally consistent and the object as a whole was nonsense. Inline
makes the two blocks one row.

What Inline does not do

A reserved feature is only honest if its limits are stated with its powers. Inline
does not do cross-entity correlation: an inlined person and an inlined address are
still independent bundles, so the person does not “live at” the address. It does not
split a collapsed same-type scope (two people flat on one object is still the deferred
case). And it loses to explicit rules, like every automatic-tier facet does (post 8).

Why reach-down lost

The deciding argument is the same spine the whole series runs on. Inline is a line
in your definition, so Explain can name it:

Office.Postal.PostalCode -> Address.PostalCode   [coherent entity]  (inlined from Postal)

Enter fullscreen mode Exit fullscreen mode

Reach-down would produce the same coherent values with no declaration anywhere to
point at. The report could only say “absorbed,” and you would have nothing in your
own code that explains why. A coherent value you cannot trace is exactly the kind of
magic the rigor lane exists to avoid.

Takeaway

Show, do not assert: a reserved operator you cannot picture is not a reservation, it
is a promissory note. And when two features meet, prefer the seam you can explain over
the convenience you cannot. Inline ships later, alongside the cross-entity work it
belongs with, but the default it sits on top of, strict and legible, is the v1.1
decision.

What’s next

This second pass through the design added exactly one decision and a lot of
consolidation. The last post is about that: the ledger of everything I have promised
but not shipped.

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다