"Given" style Implicit Conversion with inline function

Hello everyone,

I’ve tested some Scala 3 features.
I’d like to make a code with Implicit Conversion and compile-time validation with inline and error features.

For example, a Grade entity can get 0.0 to 10.0 values, but it could be assigned by Double via Implicit conversion. If the value is out of range It should throw an error in compile time.

So, I made the code bellow:

//App.scala
package a

import b.Grade

@main def init = {

  val grade1: Grade = 1.0 // this is ok
  val grade2: Grade = -1.0 // this is a compiler error

} 

// Grade.scala
package b

import scala.language.implicitConversions
import scala.compiletime.*

object Grade {

  implicit inline def doubleToGrade(d: Double): Grade = {
    if (d > -1 && d < 11) {
      Grade(d)
    } else {
      error("Grade value must be between 0 and 10")
      Grade(d)
    } 
  }
}
class Grade(d: Double)

It works like a charm. But I’m wondering if I could translate it to a brand new “Given” style of Implicit Conversion from Scala 3. I try several ways with no success.

Anyone could help me?

2 Likes

I suspect it isn’t (currently?) possible with the new style conversions, as they are given instances of the Conversion abstract class, where the implementation is packed into the apply method defined on that trait. For the compile time checking to work, that apply would also have to be inlined and it isn’t allowed to override a non-inline method with an inline method.

I tried the following:

trait InlineConversion[-T, +U]:
  inline def apply (inline x: T): U

object Grade {

  given InlineConversion[Double, Grade] = new InlineConversion[Double, Grade] {
    inline def apply(inline d: Double): Grade = 
      inline if (d > -1 && d < 11) {
        Grade(d)
      } else {
        error("Grade value must be between 0 and 10")
        Grade(d)
      } 
  }

This compiles, but it isn’t applied in Main.scala (the compiler actually says, that importing b.Grade.given_InlineConversion_Double_Grade might help, but it doesn’t). It looks like the compiler explicitly checks conversion candidates for being a subtype of scala.Conversion, which we cannot extend because of the incompatible inline in apply. So currently the old-style implicit looks like the only way for conversions with inline.

3 Likes

Thank you.

Hopefully old-style will work until we have a new-style solution.

maybe you should file an issue so it gets tracked?

How does this blend with guarding against construction of illegal instances in general? If I change this to

class Grade private (d: Double)

I get “Implementation restriction: cannot use private constructors in inlineinline methods” (sic). If I make it

opaque type Grade = Double

it becomes “Implementation restriction: No inline methods allowed where opaque type aliases are in scope”.

Aside: The input range accepted by doubleToGrade() is somewhat bigger than documented. :slight_smile:

As explained in Scala Contributors, opaque types can’t be used directly with inline method because an inline method… inlines the code at the point of use.

As a workaround, you can create your inline method in another file:

opaque type Grade = Double

object Grade {

  def unchecked(d: Double): Grade = d
}

Another file:

import Grade

//Here I use PascalCase to simulate a constructor and partially hide the true implementation
inline def Grade(d: Double): Grade = {
  //Your inline condition.
  Grade.unchecked(d) //Only this code will not be inlined. It's the only overhead you will get
}

Thanks a lot for the pointer and the example! I’ve tinkered a bit with the idea - it still feels a little convoluted, but I think I got something working at last. My example use case is spreadsheet coordinates. Here’s what I came up with so far:

object Excel:

  inline private def isValidCoords(r: Int, c: Int): Boolean =
    r >= 0 && c >= 0 && c < 256

  object ExcelCoords:

    opaque type Coords = (Int, Int)

    object Coords:
      def apply(cs: (Int, Int)): Option[Coords] =
        Option.when(isValidCoords(cs(0), cs(1)))(cs)
      private[Excel] def unsafe(cs: (Int, Int)): Coords = cs

    extension(coords: Coords)
      def row = coords(0)
      def col = coords(1)

  import ExcelCoords.*

  extension(r: Int)
    inline def \(c: Int): Coords =
      if isValidCoords(r, c)
        then Coords.unsafe(r -> c)
        else error("illegal coords column value")

Now I can do this:

import Excel.*
import ExcelCoords.*

val cs1: Coords = 16 \ 3
val cs2: Coords = 16 \ 1111 // fails to compile

Any suggestions for improvements are appreciated.