kdiff 2.0.0
Added
-
A derived differ reads a property through its Java getter, and compares 42–62% faster.
kotlin-reflect's own read goes through a reflective caller and costs about 1.9x a property reference
the compiler synthesised — which, measured, is most of the difference between a derived differ and a
hand-written one. The accessor is now resolved once, when the differ is derived.The gain depends on the model. Where reads are the whole of the work — a flat type of value
properties — 71% of the excess goes, and throughput rises 62%. On a model with nested types and
collections it is 42–44%. On a keyed list of a hundred elements, where allocation dominates, there is
no measurable change. Deriving itself costs 17% more, resolving those accessors once.Nothing is compared differently, on any route. A property whose type is a
value classkeeps the
slower read, because a JVM getter erases such a type and would yield the wrapped value rather than the
wrapper; recognising avalue classwithout being told to is unaffected. -
Deriving a differ now yields a patcher too, and a hand-written declaration can gain one.
autoDiffer<Order>()returns a value that both compares two instances and applies changes to one —
the shape@Diffablealready generates, where one declaration and one import reach both. A caller
who wroteval d: Differ<Order> = autoDiffer()is unaffected; the capability is there for whoever
reaches for it.DiffPlan.toPatcher(type)does the same for a declaration you wrote yourself: pair adiffPlan { }
with the type it describes and you have aPatchera builder could never give you, because a
declaration states how to read a type and never how to rebuild it. That closes what the
documentation presented as permanent — a@DiffWithobject that only compares no longer has to leave
its property unapplicable. → hand-written.mdRebuilding goes through the type's
copy, passing only the properties that carry changes. A
property you excluded, and one deriving could not name, keep their values without ever being read
— so this sets no accessibility flag, and a defaulted parameter is never silently reset. It is the
mechanism generated code already uses, which is what makes the two routes agree by construction
rather than by test;DerivedPatchParitySpecholds one model to one result and one failure list
through both.Applying stays partial, exactly as on the annotated route: a property delegated to a differ that
cannot rebuild its type has its changes reported and the rest of the instance still applies. -
PatchFailure.Reason.TypeNotRebuildablefor a type whosecopycannot be reached — aprivate
constructor under@ConsistentCopyVisibility. A fact about a type rather than a property, so not
UnpatchableProperty. The comparison is unaffected and still reports what differs; only putting the
result back is unavailable. Branch on the reasons you act on and give the rest anelse, as ever. -
A described subtype now names the differ it dispatches to.
PlannedSubtype.differ, alongside the
typeit already carried, the wayPlannedComparison.Nestedalready names a delegating property's
differ. A reader of a description could see that a plan dispatched and not to what.
Changed — description
-
PlannedPropertystates a property's name and how to read it, in place of the property reference.
A name is what a reported path carries and what a change is matched back to a property by, so it is
what a description of a comparison is an account of; how a value is reached is a decision about
running one. Separating the two is what lets deriving read more cheaply without any new published API.Declarations are unchanged.
field(Order::reference)still takes a property reference, on every
route. What moves isplan.properties[i].property, which becomes.name— so code matching a
description entry against a property reference stops compiling rather than starting to return
nothing. No released version offers that surface.
Changed
-
BREAKING —
autoDiffer<T>()refuses a type it cannot describe, whichever positionTis in.
The test deciding whether a type can be described property by property was applied to a property's
type and to a sealed subclass, and never to the typeautoDifferwas asked about. So the same
declaration derived at the root and was refused one level down, and for anabstractoropenbase
the root won:autoDiffer<Shape>().diff(Circle("c", "1"), Circle("c", "2")) // was []
Shapeis not sealed, so nothing dispatched toCircleand the propertyCircleadds went
uncompared — two instances that differ, reported equivalent. That call now raises
UndescribableShapeException.What you can derive for is a data class or a sealed type. An
abstract,openor ordinary class
is refused: make it a data class, make itsealedso deriving dispatches over its subclasses, or
describe it withdiffer { }. An enum, avalue classand anobjectare refused at the root for
the opposite reason — kdiff compares those whole, where they are held — so derive for the type that
holds one; as a property each is compared exactly as before. Declaring a type a value yourself with
asValue<T>()is unaffected, includingasValue<Node>()on a self-referential model, which is how
deriving is stopped from descending into one. → errors.md -
The documentation now says which route to choose, and recommends deriving.
hand-written.md
opens with a table: derive by default, annotate a comparison you have measured to be hot, and
write a declaration by hand only to splice an existing differ into a model or to describe a shape
deriving refuses. Deriving was presented as the answer to a model you would not annotate — a
fallback — which the measurements contradict.What it costs is quoted as ratios with the command that reproduces them, re-measured after the
accessor change above: a derived differ costs 1.7–2.6x a generated one, worst where nothing changed
and best six levels deep, and 1.2–1.5x a hand-written one. The band the page carried predates that
change and overstated it.api-stability.md§8 also records two decided non-features: deriving a
tracking scope, which an empty scope already does, and deriving properties visible only inside their
own declaration. → hand-written.md
Added
-
A fourth artefact,
io.github.rcapraro:kdiff-reflect, derives a differ from a type's shape.
autoDiffer<Person>()compares the properties the primary constructor states, each the way its own
type calls for — the same six comparisons@Diffableresolves, reported at the same paths — so a
domain you will not annotate needs no line per property, and a property added later is compared
with no edit. A Kotlinvalue classis recognised without being declared one.The block names only the exceptions, and each is the counterpart of one annotation:
exceptof
@DiffIgnore,asValueof@DiffAsValuein its property and its class form,nestedof
@DiffWith— which is also how a generated differ is spliced into a derived one — andkeyedList
of@DiffKey. There is deliberately nofield: a declaration naming both what to compare and what
to exclude would need a precedence rule, and such a rule decides silently which of them widens.Two things to know before adopting it. It reads shape, never kdiff annotations, so deriving a
differ for an annotated type compares what that type's annotations excluded — use the generated
differ for such a type. And it is the one part of kdiff that needskotlin-reflect: nothing
depends onkdiff-reflect, so a consumer that does not declare the coordinate carries no
reflection library, andkdiff-annotations,kdiff-runtimeand the generated code are unchanged
in that respect. Deriving applies a diff back as well as reporting one — see the entry above — and
never breaks in to do it: rebuilding goes through the type'scopyand passes only the properties
that carry changes. → hand-written.mdIt compares the constructor properties you could also name as exceptions — the
publicand
internalones — because a property you cannot write asType::propertyis one you could never
exclude, compare as one value, or delegate. Aprivateorprotectedone is skipped, and a type
stating nothing nameable is refused where the differ is derived rather than quietly comparing
nothing. Reading aninternalproperty sets no accessibility flag; that guarantee is exact for a
type your own module declares, anddocs/hand-written.mdstates the limit for a type you got from a
dependency. -
DiffPlan<T>describes what a differ compares.diffPlan { }builds the same declaration
differ { }does and returns it as a value — each property paired with the way it is compared, and
the subtypes it dispatches on — andtoDiffer()compiles it back.differ { }is now exactly
those two steps, so a plan and the differ built from it can never describe different comparisons.
A plan covers one type: a delegating property names its differ without describing it.Nothing existing moves, and no comparison costs more:
toDiffer()assembles the same closures the
builder assembled before, so nothing dispatches on the description while a comparison runs.
Coordinates
implementation("io.github.rcapraro:kdiff-annotations:2.0.0")
implementation("io.github.rcapraro:kdiff-runtime:2.0.0")
ksp("io.github.rcapraro:kdiff-processor:2.0.0")Deriving a differ from a type's shape is a separate artefact, declared only by a consumer
who uses it — it is the one part of kdiff that brings kotlin-reflect:
implementation("io.github.rcapraro:kdiff-reflect:2.0.0")