Skip to content

Generate the arithmetic operators a plain value object declares #184

Closed

Description

@CaffeinatedCoder

Problem

A plain value object over a number can't be added, subtracted or scaled without unwrapping it: Money.From(a.Value + b.Value). You can declare IAdditionOperators<Money, Money, Money> and write the operator yourself, but that is boilerplate the generator already writes for its neighbours: it generates IEqualityOperators, IComparisonOperators and IMinMaxValue<TSelf> so a value object satisfies generic-math constraints. Other value-object libraries generate arithmetic (EricksonLopez.DomainPrimitives on every numeric primitive, KB-055).

Change

  • Opt in by declaring the interface, not by an attribute and never by default: the generator implements the operators of the generic-math operator interfaces a plain IValue<T> declares, when the wrapped T implements the matching interface itself:
    public readonly partial record struct Money : IValue<decimal>,
        IAdditionOperators<Money, Money, Money>, ISubtractionOperators<Money, Money, Money>;
    Candidates: IAdditionOperators, ISubtractionOperators, IUnaryNegationOperators, IUnaryPlusOperators, and IMultiplyOperators / IDivisionOperators with the wrapped type as the right operand (Money * 3m). Which ones, and whether TSelf * TSelf is allowed, is decided in the PR. The checked operators follow the wrapped type's.
  • Plain value objects only. A validated value object gets none: an operator can't return a Result, and a sum outside Create's range would have to throw. Declared on one, the interface stays unimplemented and the build fails with CS0535, as today; the docs say so.
  • A hand-written operator is kept, and only the missing ones are generated. Code that implements these interfaces by hand on 2.3 must compile unchanged on 2.4, so this is not a CMTK1011 refusal (a minor may not report more under an Error, decision 34).
  • The Generators README's table of generated members, docs/value-objects.md ("Numbers") and the changelog name it; a claim in test/oracles/claims.json holds it.

Done when

  • A plain value object that declares an operator interface gets the operators, with the wrapped type's results (overflow and checked context included), tested over the numeric shapes of the generated-type axes.
  • A hand-written operator beside the declaration is kept, shown by a test.
  • The Native AOT smoke consumer uses one operator.

Decided (in the PR, decision 122)

  • Interfaces: IAdditionOperators, ISubtractionOperators, IMultiplyOperators, IDivisionOperators, IModulusOperators, IUnaryNegationOperators, IUnaryPlusOperators, each with its checked form where the interface has one. Left out: ++/--, the bitwise and shift operators.
  • Operands: the first type argument is the value object; the other operand and the result can each be the value object or any other type, as long as the wrapped type implements the same interface with the value object replaced by itself. So Money * 3m, Money / Money as a decimal ratio, and TSelf * TSelf when declared.
  • How: each operator calls the wrapped type's through GeneratedArithmetic (base package), a constrained generic call, so byte stays byte and char's explicit operators are reached.
  • Measured on the way: DateTime, TimeSpan, DateOnly and Guid implement none of the operator interfaces, so a value object over them gets none (CS0535). A lone hand-written checked operator keeps failing (CS9025). In EF Core an operator between two value objects translates; one with another operand throws (EF Core: translate an arithmetic operator whose other operand or result is not the value object #186), and a hand-written one would run as the database's arithmetic (EF Core: a hand-written arithmetic operator in a query runs as the database's arithmetic #187).

Found in review (#188)

  • An interface's explicit implementation was replaced. An interface that implements + explicitly, such as IAddsLeft<TSelf> : IAdditionOperators<TSelf, TSelf, TSelf>, is what every type declaring it reaches by default. The preservation check read the value object and its base records only, so a public + was generated beside it: generic addition read 3 where the interface's answered 1. Kept now, with its checked form, through a generic interface, one further down, a base record's interface and a single value object's (ArithmeticTests.An_interfaces_explicit_implementation_is_kept). The same gap in the members generated since before 2.4, IMinMaxValue and IFormattable measured, is An interface's explicit implementation of a generated interface member is replaced without a diagnostic #191.

Activity

  1. added this to the 2.4.0 milestone on Oct 30, 2026
  2. CaffeinatedCoder commented on Oct 31, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Merged into release/2.4.0 by #188 (ded5fa6).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/value-objectsValue-object contracts and what the aspects generate for themenhancementNew feature or requestpackage/generatorsAffects CodoMetis.TypeKit.Generators

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions