Skip to content

IVectorN.Length() returns bare T, dropping the dimension at the one point the caller wants it #238

Description

@matt-edmondson

CLAUDE.md states: "IVectorN.Magnitude() (for N >= 1) returns the corresponding IVector0." The C++ projection does exactly that — each form answers magnitude() with the magnitude form of the same dimension, and the doc makes a point of how nicely the dimension works out through sqrt.

The C# side does not. IVector3<TVector, T> declares:

public T Length();
public T Distance(TVector other);

So position.Length() on a Position3D<T> hands back a bare T. The dimension is discarded precisely where the caller wanted Length<T> — or Distance<T> — and has to be re-wrapped by hand, which is the untyped step the library exists to remove. The same applies to IVector2 and IVector4.

Checked across all 72 entries in dimensions.json: every dimension that declares a vector form also declares a vector0, so there is no case where the magnitude type is unavailable.

Design

Add alongside the existing members without changing them — Length() and Distance() stay, since generated code and consumers use them:

/// <summary>Gets the magnitude of this vector as its dimension's magnitude quantity.</summary>
public global::ktsu.Semantics.Quantities.Length<T> Magnitude()
    => global::ktsu.Semantics.Quantities.Length<T>.Create(StorageMath.Sqrt(LengthSquared()));

/// <summary>Gets the distance to another vector as its dimension's magnitude quantity.</summary>
public global::ktsu.Semantics.Quantities.Length<T> DistanceTo(Position3D<T> other)
    => global::ktsu.Semantics.Quantities.Length<T>.Create(StorageMath.Sqrt(DistanceSquared(other)));

Four things to settle

1. Return the V0 base, not an overload. Displacement3D<T>.Magnitude() returns Length<T>, not Distance<T>, because the base is what every overload widens from (resolved decision 3 in CLAUDE.md). A caller who wants Distance narrows explicitly. That also keeps the generator rule purely mechanical: read quantities.vector0.base.

2. A name collision the generator has to handle. Displacement3D<T> already has a method called Length(), so writing Length<T> as a return type inside that same class puts a method group and a generic type under one identifier. It parses — the type argument list disambiguates — but it is fragile and unreadable, and it will confuse anyone reading the generated source. Emit the fully qualified name, as above. This is the one non-obvious implementation detail in the issue.

3. Do not emit MagnitudeSquared(). C++ answers it with a bare Quantity<D> for the honest reason that the square of a dimension usually has no name, and where it has one it is not unique. C# has no structural layer to fall back on, and Velocity² has no declared name at all — so there is nothing to return. Leave LengthSquared() returning T, which is the correct answer rather than a missing feature.

4. Magnitude() uses Create, not a From{Unit} factory, so it bypasses Vector0Guards. That is correct — a magnitude is non-negative by construction — and it avoids paying for a guard on every call in a hot loop.

Tests

  • For each vector form, Magnitude() equals the V0 constructed from Length()
  • The return type is the dimension's V0 base — a compile-time check, so an explicitly typed local is enough
  • Magnitude() of a zero vector equals the V0's Zero
  • DistanceTo agrees with Magnitude() of the difference, for the signed forms where subtraction is defined
  • Cover at least one dimension whose V0 base name differs from the dimension name (Velocity3D → Speed, Acceleration3D → AccelerationMagnitude), since that is where a naive generator rule using the dimension name instead of the V0 base would break

Activity

  1. matt-edmondson commented on Sep 16, 2026

    @matt-edmondson
    ContributorAuthor

    Triage

    Category: Improvement
    Priority: Medium
    Area: Semantics.Quantities — IVector2/IVector3/IVector4 magnitude members

    Medium: the dimension is discarded at precisely the point the caller wanted it, and the caller re-wraps by hand — which is the untyped step the library exists to remove. What lifts this above a nice-to-have is that it is a documented contract the C# side does not honour: CLAUDE.md states IVectorN.Magnitude() returns the corresponding IVector0, and the C++ projection does exactly that. The two projections disagree, and the docs describe the C++ one.

    The issue also closes off the obvious objection — all 72 entries in dimensions.json that declare a vector form also declare a vector0, so there is no case where the magnitude type is unavailable.

    Suggested assignment: none specific.

    Related: #237 is the same shape of gap on the construction side (no From{Unit} factories for vectors) where this is on the reading side. Both are the vector forms not receiving what the scalars already have; a consumer hits them in the same session. Worth sequencing together even though they are separate changes.

    Open PR covering this: none.

    Suggested next step: all four sub-decisions look right, and the one to make sure survives implementation is #2 — emitting the fully qualified return type. Displacement3D<T> already has a Length() method, so an unqualified Length<T> return type puts a method group and a generic type under one identifier in the same class. It parses, which is the problem: it will compile, and then confuse everyone who reads the generated source.

    Decision #1 (return the V0 base, not an overload) is also what keeps the generator rule mechanical — read quantities.vector0.base — and the test the issue asks for covering dimensions whose V0 base name differs from the dimension name (Velocity3D → Speed, Acceleration3D → AccelerationMagnitude) is exactly the case a naive rule using the dimension name would break on. That test is the one worth writing first.


    Generated by Claude Code

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

Metadata

Metadata

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions