Skip to content

Conversation

@jddarcy
Copy link
Member

@jddarcy jddarcy commented Jan 16, 2026

A refinement of some earlier ideas on the numerics modeling interfaces to inform further discussions.

This is a "lumpy" rather than "splitty" design in terms of favoring a smaller number of interfaces with more functionality rather than a larger number of interfaces making smaller distinctions.

Various design comments and to-do's noted in the code.


Progress

  • Change must not contain extraneous whitespace

Issue

  • JDK-8338529: Initial iteration of numerics modeling interfaces (Enhancement - P4)

Reviewing

Using git

Checkout this PR locally:
$ git fetch https://git.openjdk.org/valhalla.git pull/1917/head:pull/1917
$ git checkout pull/1917

Update a local copy of the PR:
$ git checkout pull/1917
$ git pull https://git.openjdk.org/valhalla.git pull/1917/head

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 1917

View PR using the GUI difftool:
$ git pr show -t 1917

Using diff file

Download this PR as a diff file:
https://git.openjdk.org/valhalla/pull/1917.diff

Using Webrev

Link to Webrev Comment

@bridgekeeper
Copy link

bridgekeeper bot commented Jan 16, 2026

👋 Welcome back darcy! A progress list of the required criteria for merging this PR into type-classes will be added to the body of your pull request. There are additional pull request commands available for use with this pull request.

@openjdk
Copy link

openjdk bot commented Jan 16, 2026

@jddarcy This change now passes all automated pre-integration checks.

ℹ️ This project also has non-automated pre-integration requirements. Please see the file CONTRIBUTING.md for details.

After integration, the commit message for the final commit will be:

8338529: Initial iteration of numerics modeling interfaces

You can use pull request commands such as /summary, /contributor and /issue to adjust it as needed.

At the time when this comment was updated there had been no new commits pushed to the type-classes branch. If another commit should be pushed before you perform the /integrate command, your PR will be automatically rebased. If you prefer to avoid any potential automatic rebasing, please check the documentation for the /integrate command for further details.

➡️ To integrate this PR with the above commit message to the type-classes branch, type /integrate in a new comment.

@openjdk openjdk bot added ready Pull request is ready to be integrated rfr Pull request is ready for review labels Jan 16, 2026
@mlbridge
Copy link

mlbridge bot commented Jan 16, 2026

Webrevs

* @param op1 the first operand
* @param op2 the second operand
*/
OC min(OC op1, OC op2);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a default method?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure if min/max should be included in the interface. I think it depends what role any non-operator methods have in interfaces intended for witnesses.

However, as long as the methods are here, they could be default methods, yes.

To acknowledge some feedback received on a different channel, if the OC type variable were required to extend Comparable, then all the lessThan, greaterThan, etc. method could have default methods written in terms of the results of compareTo. However, if we want to move away from Comparable, then extended its usage here probably isn't a good idea.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure if min/max should be included in the interface. I think it depends what role any non-operator methods have in interfaces intended for witnesses.

However, as long as the methods are here, they could be default methods, yes.

To acknowledge some feedback received on a different channel, if the OC type variable were required to extend Comparable, then all the lessThan, greaterThan, etc. method could have default methods written in terms of the results of compareTo. However, if we want to move away from Comparable, then extended its usage here probably isn't a good idea.

PS Pushed another changeset refactoring to have less than be the base operation and using default methods for the rest.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m not sure what the intended design rules are here, but for anything algebraic, fewer axioms are much better than more (murkily related) axioms. Having many abstract interface points is the same as many axioms — the witnesses each have their own story for each API point independently.

The default method approach is very wise, and the body of the default should be in an implnote in the docs. It amounts to a proof that shows how to (e.g.) deduce min and max from lower-level axioms.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m not sure what the intended design rules are here, but for anything algebraic, fewer axioms are much better than more (murkily related) axioms. Having many abstract interface points is the same as many axioms — the witnesses each have their own story for each API point independently.

The default method approach is very wise, and the body of the default should be in an implnote in the docs. It amounts to a proof that shows how to (e.g.) deduce min and max from lower-level axioms.

To be precise, for a default method in an interface (unless various special conditions hold*), there should be an implSpec tag (not implNote) that at least documents the general logic and calls to other methods of the interface used in the default method's body. Replicating the full body of the method in the implSpec tag can be acceptable, especially if it is a one-liner, but it is not requited in general. We have multiple examples of this kind of using the JDK APIs, including in javax.lang.model where I'm one of the maintainers.

The implSpec tags I included in the changeset to Orderable for it to have default methods follow the general conventions of tag usage for that situation in the JDK.

Thanks and HTH.

(* The special conditions where an implSpec tag on a default method can be skipped include cases like a default method of a sealed interface where there will be no implementations outside of the shared maintenance domain of the interface.)

Copy link
Member Author

@jddarcy jddarcy Jan 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m not sure what the intended design rules are here, but for anything algebraic, fewer axioms are much better than more (murkily related) axioms. Having many abstract interface points is the same as many axioms — the witnesses each have their own story for each API point independently.

The default method approach is very wise, and the body of the default should be in an implnote in the docs. It amounts to a proof that shows how to (e.g.) deduce min and max from lower-level axioms.

One of the implicit design questions here is "are these the interfaces that allow operator overloading?" or "are these the interfaces that declare various algebraic properties?"

Since many of the numerical types of possible interest are closer to, say, floating-point-like types that have few algebraic properties rather than integer-like types that have more, I don't want to preclude floating-point-like types from participating in operator overloading because of their lack of strong algebraic properties. Matrices/vectors would also fall closer to floating-point-like rather than integer-like and I would not want to preclude matrices/vectors from benefiting from operators either.

I included a slide in my 2025 JVMLS talk on numerics speculating that a future refinement of these kinds of interfaces could include an idiom to indicate "yes, this type actually obeys the ring axioms" or "... the field axioms", etc., but that is not included in this early "lumpy" iteration.

I would fully expect some evolution of the set of interfaces, what methods go where, the set of interfaces, etc. as we can more experience using these trial types with type classes.

Thanks.

@rose00
Copy link
Collaborator

rose00 commented Jan 17, 2026

I would prefer to see the ordering part tolerating partial orders (NaNs), and then factor the ordering part on top of both ints and floats.

Also, in the name of reducing primitives, I think a compare(x,y) method is the correct primitive. It would return one of "lt", "gt", "eq", and (only for FP orders) "uo". All the others, up to min/max, can be built on top as default methods, even if they also end up hardwired to an override, for efficiency.

@rose00
Copy link
Collaborator

rose00 commented Jan 17, 2026

interface Orderable<T> {

enum Order { LT, GT, EQ, UO };  //or use a couple int-bits

/** Return LT if x definitely precedes y.
 * Return GT if y definitely precedes x.
 * Return EQ if x and y are equivalent <i>in this particular order</i>
 * Return UO otherwise
 * Conformance rules are as follows.
 * Symmetry: If order(x,y)==LT (resp. GT, EQ, UO)  then also order(y,x)==GT (resp. LT, EQ, UO)
 * Reflexivity: Always order(x,y)==EQ or UO, never GT or LT.
 * Transitivity: If order(x,y)==LT or EQ and order(y,z)==LT or EQ, then order(x,z)==LT or EQ, but cannot be EQ if both previous were EQ.
 */
abstract Order order(T x, T y);
// This is enough to drive a sort algorithm with care, even though it’s just partial.

/**
 * Returns true if this order never reports UO for any input.
 * True for integers and anything comparable or having a comparator.
 * False for FP types (because of NaN elements).
 * False for partially ordered data types, if not totalized.
 */
abstract boolean isTotal();

/**
 * If this returns true for two x and y, then order(x,y) is not UO.
 */
default boolean isTotal(T x) {
  return isTotal() || order(x,x) != UO;
}

// Could do similar query to detect the less interesting case of
// orders which are coarser than operator==, but that’s only
// true with +0.0 vs. -0.0.

default boolean lt(T x, T y) { return order(x,y) == LT; }
default boolean le(T x, T y) { return lt(x,y) || eq(x,y); }
//... and so forth for gt, ge, eq, ne

default boolean min(T x, T y) { return lt(x,y) ? x : y; }
//... and so forth
}

@mlbridge
Copy link

mlbridge bot commented Jan 17, 2026

Mailing list message from Brian Goetz on valhalla-dev:

Overall I agree with the direction that while we should be _inspired by_
algebraic structures, we should not slavishly adhere to them, because
these are not platonic numbers, and they fail to obey the algebraic
axioms in all sorts of ways.

I think we can borrow much hard-won experience from our friends in the
Haskell community.? For example, `Num a` defines both (+) and (*), and
suggests the existence of additive and multiplicative identities, but
appeals not to ring-ness but to something more like "be reasonable".?
Specifically, it says "looks like a ring, lots of Nums are rings, but
actually requiring it would be too restrictive."? And despite the
seeming complexity of the Haskell numeric tower, there is much
(sensible) lumping going on in the base Num class.? (All of these
properties seem like candidates for shameless stealing.)

Haskell also relegates the algebraic structure (ring, monoid, etc) to a
different corner of the field (heh), separate from the types describing
numerics, and doesn't attempt to use symbolic operators, instead using
nominal functions like `mzero`.

The point about implementing both `Num` and `Ord` is one of those
"obvious not obvious" statements; given that an ordered ring includes
additional axioms above the union of the ring and ordering axioms, it
would not be reasonable to expect "witnesses Ord" and "witnesses Ring"
to mean "witnesses OrderedRing".? Instead, one would need an OrderedRing
type class, which extends Ord and Eq, and adds additional laws.

The Haskell Report defines no laws for |Num
<https://hackage-content.haskell.org/package/base-4.22.0.0/docs/GHC-Num.html#t:Num>|.
However, |(|+
<https://hackage-content.haskell.org/package/base-4.22.0.0/docs/GHC-Num.html#v:-43->|)|
and |(|*
<https://hackage-content.haskell.org/package/base-4.22.0.0/docs/GHC-Num.html#v:-42->|)|
are customarily expected to define a ring and have the following
properties:

*Associativity of |(|+
<https://hackage-content.haskell.org/package/base-4.22.0.0/docs/GHC-Num.html#v:-43->|)|*
|(x + y) + z| = |x + (y + z)|
*Commutativity of |(|+
<https://hackage-content.haskell.org/package/base-4.22.0.0/docs/GHC-Num.html#v:-43->|)|*
|x + y| = |y + x|
*||fromInteger
<https://hackage-content.haskell.org/package/base-4.22.0.0/docs/GHC-Num.html#v:fromInteger>|
0| is the additive identity*
|x + fromInteger 0| = |x|
*|negate
<https://hackage-content.haskell.org/package/base-4.22.0.0/docs/GHC-Num.html#v:negate>|
gives the additive inverse*
|x + negate x| = |fromInteger 0|
*Associativity of |(|*
<https://hackage-content.haskell.org/package/base-4.22.0.0/docs/GHC-Num.html#v:-42->|)|*
|(x * y) * z| = |x * (y * z)|
*||fromInteger
<https://hackage-content.haskell.org/package/base-4.22.0.0/docs/GHC-Num.html#v:fromInteger>|
1| is the multiplicative identity*
|x * fromInteger 1| = |x| and |fromInteger 1 * x| = |x|
*Distributivity of |(|*
<https://hackage-content.haskell.org/package/base-4.22.0.0/docs/GHC-Num.html#v:-42->|)|
with respect to |(|+
<https://hackage-content.haskell.org/package/base-4.22.0.0/docs/GHC-Num.html#v:-43->|)|*
|a * (b + c)| = |(a * b) + (a * c)| and |(b + c) * a| = |(b * a) +
(c * a)|
*Coherence with |toInteger|*
if the type also implements |Integral
<https://hackage-content.haskell.org/package/base-4.22.0.0/docs/GHC-Real.html#v:Integral>|,
then |fromInteger
<https://hackage-content.haskell.org/package/base-4.22.0.0/docs/GHC-Num.html#v:fromInteger>|
is a left inverse for |toInteger
<https://hackage-content.haskell.org/package/ghc-internal-9.1401.0/docs/GHC-Internal-Real.html#v:toInteger>|,
i.e. |fromInteger (toInteger i) == i|

Note that it /isn't/ customarily expected that a type instance of both
|Num
<https://hackage-content.haskell.org/package/base-4.22.0.0/docs/GHC-Num.html#t:Num>|
and |Ord
<https://hackage-content.haskell.org/package/base-4.22.0.0/docs/Data-Ord.html#t:Ord>|
implement an ordered ring. Indeed, in |base| only |Integer
<https://hackage-content.haskell.org/package/base-4.22.0.0/docs/Prelude.html#t:Integer>|
and |Rational
<https://hackage-content.haskell.org/package/base-4.22.0.0/docs/Data-Ratio.html#v:Rational>|
do.

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/valhalla-dev/attachments/20260117/5b6e4288/attachment-0001.htm>

package java.lang;

/**
* Indicates a type supports the basic binary arithmetic operations of

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Indicates a type supports the basic binary arithmetic operations of
* Indicates a type that supports the basic binary arithmetic operations of

@jddarcy
Copy link
Member Author

jddarcy commented Jan 20, 2026

From experience working on #1937, pushed another changeset to this PR discussion closure properties of different operations and how they are expected to be documented. In other words, if value is not returned for a particular combination of operands, the expectation is that an ArithmeticException will be thrown and the conditions under which that exception would be thrown should be documented in the numerical type.

@jddarcy
Copy link
Member Author

jddarcy commented Jan 21, 2026

I would prefer to see the ordering part tolerating partial orders (NaNs), and then factor the ordering part on top of both ints and floats.

Also, in the name of reducing primitives, I think a compare(x,y) method is the correct primitive. It would return one of "lt", "gt", "eq", and (only for FP orders) "uo". All the others, up to min/max, can be built on top as default methods, even if they also end up hardwired to an override, for efficiency.

To make progress on getting experience using type classes, I'm going to push the current state of the PR. Reconsidering how ordering is handled be done as future work, along with other refinements of the modeling interfaces. Thanks.

@jddarcy
Copy link
Member Author

jddarcy commented Jan 21, 2026

/integrate

@openjdk
Copy link

openjdk bot commented Jan 21, 2026

Going to push as commit 7a8dc9c.

@openjdk openjdk bot added the integrated Pull request has been integrated label Jan 21, 2026
@openjdk openjdk bot closed this Jan 21, 2026
@openjdk openjdk bot removed ready Pull request is ready to be integrated rfr Pull request is ready for review labels Jan 21, 2026
@openjdk
Copy link

openjdk bot commented Jan 21, 2026

@jddarcy Pushed as commit 7a8dc9c.

💡 You may see a message that your pull request was closed with unmerged commits. This can be safely ignored.

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

Labels

integrated Pull request has been integrated

Development

Successfully merging this pull request may close these issues.

4 participants