Modeling general physical quantities after SIP-56

SIP-56, to be realized in Scala 3.4.0, introduces some new restrictions to match type reduction.

As it stands, the type-safe zero-overhead physical dimensions library I wrote makes use of match types that will become illegal (according to Scala 3.4.0-RC1 and Nightly compilers).

Can anyone think of a way to make it compliant with SIP-56?

Below is a minimal toy version to illustrate the issue (Scastie link):

object OpaqueScope:
  /**
   * Type that keeps track of the dimension of a quantity.
   *
   * @tparam Length exponent for the length component of the dim
   * @tparam Frequency exponent for the frequency component of the dim
   */
  opaque type Dim[Length <: NatT, Frequency <: NatT] = Double

  // Physical quantity representing one metre, aka SI unit for length
  val metre: Length = 1.0
  // Physical quantity representing one Hertz, aka SI unit for frequency
  val hertz: Frequency = 1.0

  extension[L <: NatT, F <: NatT] (x: Dim[L, F])
    // Generic function to multiply any two physical quantities.
    def *[Ly <: NatT, Fy <: NatT](y: Dim[Ly, Fy]): Dim[L, F] * Dim[Ly, Fy] = x * y
end OpaqueScope

import OpaqueScope.*

// Multiplies the two given Dims.
type *[D1, D2] = D1 match
  case Dim[l1, f1] => D2 match
    case Dim[l2, f2] => Dim[Sum[l1, l2], Sum[f1, f2]]

// Convenience alias for the type of physical quantities representing lengths
type Length = Dim[Succ[Zero], Zero]
// Convenience alias for the type of physical quantities representing frequencies
type Frequency = Dim[Zero, Succ[Zero]]
// Convenience alias for the type of physical quantities representing velocities
type Velocity = Length * Frequency

// Physical quantity multiplication example
val oneMetrePerSecond: Velocity = metre * hertz

// Type-level natural numbers
sealed trait NatT
case class Zero() extends NatT
case class Succ[N <: NatT](n: N) extends NatT

// Type-level natural-number sum
type Sum[M <: NatT, N <: NatT] <: NatT = (M, N) match
  case (_, Zero) => M
  case (Zero, _) => N
  case (Succ[predM], Succ[predN]) => Succ[Succ[Sum[predM, predN]]]

The issue is that * — the generic function to multiply any two physical quantities — has to be defined within OpaqueScope in order to be able to treat its parameters as doubles.

On the other hand, since it is generic, its return type has to be computed using a match type, which is done with the type-level function *.

However, since Dim is synonymous with Double within OpaqueScope, according to SIP-56, Dim[L, F] * Dim[Ly, Fy] cannot be reduced, leading to a compile-error.

Any thoughts would be greatly appreciated.

Note: this question is a continuation of the conversation with @sjrd that started on this issue.

Typelevel arithmetic is best to accomplish directly with scala.compiletime.ops.

Regarding your library, also see GitHub - erikerlandson/coulomb: coulomb: unit analysis for Scala

@soronpo Thanks! Do you know any example of type-level arithmetic in scala.compiletime.ops? Also, this wouldn’t solve the issue, right?

I didn’t know about Coulomb, thanks for sharing. It seems to have a rather different focus, but looks really well done. It is definitely much more developed than my project.

Coulomb looks interesting. What are its performance characteristics? Does it have a runtime overhead? I took a quick look at the docs, but I didn’t see anything about that, though I may have missed it.

I just thought of a potential solution to my issue: don’t make Dim the opaque type, but rather wrap it with a new opaque type to represent physical quantities. I haven’t tried it yet, but, other than adding boilerplate, it looks like it might just work.

I’m still interested in reading alternative proposals, though!

Since it’s generic, instead of being specialized to Double, I would think it comes with significant overhead. I couldn’t find the actual implementation for e.g. Mul for Double in the code, though, so I might be missing something.

https://dotty.epfl.ch/docs/reference/metaprogramming/compiletime-ops.html#the-scalacompiletimeops-package-1

Should work just fine for your usecase, IIUC.

I don’t know. You can try asking on the project repo.

scala.compiletime.ops currently doesn’t seem up to the task.

See this limitation and this bug.

See here and here.