Skip to content

Conversation

sjrd
Copy link
Member

@sjrd sjrd commented Sep 3, 2025

It has always been weird that the public API of primitive types, enriched by Predef depends on the Rich* classes, which are in the not-really-public runtime package.

Moreover, the design of those classes was overabstracted, with consequences on performance (unnecessary boxing) and quality of the API (including methods that do not make sense on some types). To mitigate that, the individual Rich* classes redefined some (but not all) of the methods, defeating the abstraction.

We solve both issues with a simple solution: define all those methods as simple extension methods. We do this directly in the companion objects of the primitive types.


This would be a required step before we do anything about #23824.

@hamzaremmal
Copy link
Member

@sjrd you have beaten me to it 😄

@soronpo
Copy link
Contributor

soronpo commented Sep 4, 2025

The problem is that extension methods are not equivalent to implicit classes/definition extensions. Without solving https://contributors.scala-lang.org/t/relaxed-extension-methods-sip-54-are-not-relaxed-enough/6585/1 this change will break everywhere that has methods in the same name in scope.

@sjrd
Copy link
Member Author

sjrd commented Sep 4, 2025

The problem is that extension methods are not equivalent to implicit classes/definition extensions. Without solving https://contributors.scala-lang.org/t/relaxed-extension-methods-sip-54-are-not-relaxed-enough/6585/1 this change will break everywhere that has methods in the same name in scope.

I don't see how that problem appears when defining extensions for monomorphic types. You wouldn't redefine another extension of toHexString for Int (or if you do, it's on you), so you won't have a clash like that. With polymorphic types there are reasons to define separate extensions for Foo[Int] and Foo[String], but for a monomorphic type that's not the case.

Comment on lines +495 to +496
@deprecated("isWhole on Byte is always true", "2.12.15")
def isWhole: Boolean = true
Copy link
Member

Choose a reason for hiding this comment

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

I understand why this was done, but it feels weird to say that an extension method was deprecated since 2.12.15 😅

@soronpo
Copy link
Contributor

soronpo commented Sep 4, 2025

The problem is that extension methods are not equivalent to implicit classes/definition extensions. Without solving https://contributors.scala-lang.org/t/relaxed-extension-methods-sip-54-are-not-relaxed-enough/6585/1 this change will break everywhere that has methods in the same name in scope.

I don't see how that problem appears when defining extensions for monomorphic types. You wouldn't redefine another extension of toHexString for Int (or if you do, it's on you), so you won't have a clash like that. With polymorphic types there are reasons to define separate extensions for Foo[Int] and Foo[String], but for a monomorphic type that's not the case.

You are right. I thought the implementation used a typeclass approach + extension methods. My mistake.

*
* @param end The final bound of the range to make.
*/
def until(end: Byte): NumericRange.Exclusive[Byte] = NumericRange(self, end, 1)
Copy link
Member

Choose a reason for hiding this comment

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

Since it is part of the very little subset where everyone agrees on it :). Same goes for to.

Suggested change
def until(end: Byte): NumericRange.Exclusive[Byte] = NumericRange(self, end, 1)
infix def until(end: Byte): NumericRange.Exclusive[Byte] = NumericRange(self, end, 1)

Copy link
Member Author

Choose a reason for hiding this comment

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

This PR is strictly a refactoring. Adding infix anywhere should be done separately.

Copy link
Member

Choose a reason for hiding this comment

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

If we view the PR this way, then okay.

Comment on lines +525 to +528
def max(that: Byte): Byte = java.lang.Math.max(self.toInt, that.toInt).toByte

/** Returns `this` if `this < that` or `that` otherwise. */
def min(that: Byte): Byte = java.lang.Math.min(self.toInt, that.toInt).toByte
Copy link
Member

Choose a reason for hiding this comment

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

(my personal preferences here)

Suggested change
def max(that: Byte): Byte = java.lang.Math.max(self.toInt, that.toInt).toByte
/** Returns `this` if `this < that` or `that` otherwise. */
def min(that: Byte): Byte = java.lang.Math.min(self.toInt, that.toInt).toByte
infix def max(that: Byte): Byte = java.lang.Math.max(self.toInt, that.toInt).toByte
/** Returns `this` if `this < that` or `that` otherwise. */
infix def min(that: Byte): Byte = java.lang.Math.min(self.toInt, that.toInt).toByte

@hamzaremmal
Copy link
Member

@sjrd The test suite has been merged. It is missing the CB, sbt scripted test and Scala.js tests. I know this is waiting for the test suite, so you can consider rebasing this branch on top of 6c86743.

@tewecske
Copy link

tewecske commented Oct 3, 2025

Will this change be available in 3.3.x LTS?

@hamzaremmal
Copy link
Member

Will this change be available in 3.3.x LTS?

Unfortunately, it is not possible to be backported to 3.3.x.

@sjrd
Copy link
Member Author

sjrd commented Oct 3, 2025

No.

@sjrd sjrd force-pushed the primitive-extensions branch 2 times, most recently from 51be203 to bb19ec5 Compare October 8, 2025 09:41
@sjrd sjrd marked this pull request as ready for review October 9, 2025 03:45
@sjrd sjrd requested a review from a team as a code owner October 9, 2025 03:45
@sjrd
Copy link
Member Author

sjrd commented Oct 9, 2025

@WojciechMazur Would you mind running an open community build in this PR? It can affect source compatibility pretty strongly, so I'd like to gather cases where we can improve it.

@WojciechMazur
Copy link
Contributor

The first run of OpenCB was ineffective - we've finally started to require to use new minimal JDK version, so ~300 projects failed with 8 is not a valid choice for -java-output-version.
I'll need to adjust OpenCB to filter out or overwrite this flag (an its aliases) before running it again. I should have the results tomorrow.

@hamzaremmal
Copy link
Member

The first run of OpenCB was ineffective - we've finally started to require to use new minimal JDK version, so ~300 projects failed with 8 is not a valid choice for -java-output-version.

I'll need to adjust OpenCB to filter out or overwrite this flag (an its aliases) before running it again. I should have the results tomorrow.

We have been compiling the stdlib with Java 17 for two months now, it's just about compiling those projects with Java 17, not 8 (shouldn't be a problem unless weird project specific issues)

@WojciechMazur
Copy link
Contributor

We have been compiling the stdlib with Java 17 for two months now, it's just about compiling those projects with Java 17, not 8 (shouldn't be a problem unless weird project specific issues)

In OpenCommmunityBuild we've been compiling projects using JDK 17 since 3.7 so that's not an issue. JDK 8 is currently only used for the 3.3 LTS OpenCB builds. For nightlies we use either 17 or 21 - version is picked based on GitHub workflows defined for project. We'd be adding JDK 25 soon as well.

What has changed recently is we've finally started to enforce the new minimal version in #24146. Until that point --java-output-version:8 was valid and as the results show it was pretty popular (~16% of projects was setting JDK compatibility for 16 or lower).

The fix is on the way, I'm testing it locally, should be able to run OpenCB with it later today.

…hods.

It has always been weird that the public API of primitive types,
enriched by `Predef` depends on the `Rich*` classes, which are
in the not-really-public `runtime` package.

Moreover, the design of those classes was overabstracted, with
consequences on performance (unnecessary boxing) and quality of
the API (including methods that do not make sense on some types).
To mitigate that, the individual `Rich*` classes redefined some
(but not all) of the methods, defeating the abstraction.

We solve both issues with a simple solution: define all those
methods as simple `extension` methods. We do this directly in the
companion objects of the primitive types.
@sjrd sjrd force-pushed the primitive-extensions branch from bb19ec5 to e1ec5ec Compare October 13, 2025 09:13
@WojciechMazur
Copy link
Contributor

When compared with latest nightly 3.8.0-RC1-bin-20251010-7b7f2f1-NIGHTLY there are 7 projects affected by this change (there might be more but these were already failing when using latest nightly)

Project Version Build URL Notes
akka/alpakka 6.0.2 Open CB logs
avokka/avokka 0.0.8 Open CB logs
business4s/decisions4s 0.1.2 Open CB logs
herminiogg/shexml 0.6.1 Open CB logs
indoorvivants/genovese 0.0.3 Open CB logs
input-output-hk/scrypto 3.1.0 Open CB logs
sirthias/parboiled2 2.5.1 Open CB logs

@sjrd
Copy link
Member Author

sjrd commented Oct 13, 2025

Thanks. There are definitely a few things to improve there.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Scala 2 vs Scala 3 compiler regression when calling isNaN on a double

5 participants