Skip to content

kdiff 2.0.0

Choose a tag to compare

@github-actions github-actions released this 18 Sep 17:51
· 4 commits to main since this release

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 class keeps the
    slower read, because a JVM getter erases such a type and would yield the wrapped value rather than the
    wrapper; recognising a value class without 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 @Diffable already generates, where one declaration and one import reach both. A caller
    who wrote val 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 a diffPlan { }
    with the type it describes and you have a Patcher a 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 @DiffWith object that only compares no longer has to leave
    its property unapplicable. → hand-written.md

    Rebuilding 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; DerivedPatchParitySpec holds 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.TypeNotRebuildable for a type whose copy cannot be reached — a private
    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 an else, as ever.

  • A described subtype now names the differ it dispatches to. PlannedSubtype.differ, alongside the
    type it already carried, the way PlannedComparison.Nested already names a delegating property's
    differ. A reader of a description could see that a plan dispatched and not to what.

Changed — description

  • PlannedProperty states 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 is plan.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 position T is 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 type autoDiffer was asked about. So the same
    declaration derived at the root and was refused one level down, and for an abstract or open base
    the root won:

    autoDiffer<Shape>().diff(Circle("c", "1"), Circle("c", "2"))   // was []

    Shape is not sealed, so nothing dispatched to Circle and the property Circle adds 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, open or ordinary class
    is refused: make it a data class, make it sealed so deriving dispatches over its subclasses, or
    describe it with differ { }. An enum, a value class and an object are 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, including asValue<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 @Diffable resolves, 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 Kotlin value class is recognised without being declared one.

    The block names only the exceptions, and each is the counterpart of one annotation: except of
    @DiffIgnore, asValue of @DiffAsValue in its property and its class form, nested of
    @DiffWith — which is also how a generated differ is spliced into a derived one — and keyedList
    of @DiffKey. There is deliberately no field: 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 needs kotlin-reflect: nothing
    depends on kdiff-reflect, so a consumer that does not declare the coordinate carries no
    reflection library, and kdiff-annotations, kdiff-runtime and 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's copy and passes only the properties
    that carry changes. → hand-written.md

    It compares the constructor properties you could also name as exceptions — the public and
    internal ones — because a property you cannot write as Type::property is one you could never
    exclude, compare as one value, or delegate. A private or protected one is skipped, and a type
    stating nothing nameable is refused where the differ is derived rather than quietly comparing
    nothing. Reading an internal property sets no accessibility flag; that guarantee is exact for a
    type your own module declares, and docs/hand-written.md states 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 — and toDiffer() 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")