Skip to content

Commit

Permalink
Merge pull request #64 from isaacl/optimizeTotalWrapper
Browse files Browse the repository at this point in the history
Optimize TotalWrapper
  • Loading branch information
ornicar authored Oct 15, 2024
2 parents acc5262 + 8dc37c8 commit 9f63ff6
Showing 1 changed file with 123 additions and 43 deletions.
166 changes: 123 additions & 43 deletions core/src/main/scala/newtypes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,50 +7,86 @@ import alleycats.Zero
// thanks Anton!
// https://github.com/indoorvivants/opaque-newtypes/blob/main/modules/core/src/main/scala/OpaqueNewtypes.scala

// WARNING: This implementation below is fragile and seemingly small changes can degrade performance.
// In particular, do not add any methods which implicitly use the witness to perform type conversions.
// Only use `raw` and `apply` (or the extension methods) to convert types.
//
// Details: The classes use witnesses to ensure type compatibility. However, in order to completely inline
// simple methods like `<` (OpaqueInt), we cannot use the witness, and instead "cast" using asInstanceOf.
// During compilation, scala3 detects and removes redundant casts when using asInstanceOf, but it does *not*
// detect or remove redudant witness casts. So, when using asInstanceOf, types are completely elided, but not
// so with witnesses. This is especially problematic for methods marked `inline` which should be small.
//
// The challenge for you, dear coder, when writing new code, either in this file or in a subclass, is that
// you can accidentally rely on a witness cast, because scala will happily use a witness implicitly for
// conversions. It's up to you to ensure any new code does not use a witness conversion (use `raw`, `apply`,
// or extension methods instead).
//
// === Why this issue is not regression tested ===
// - The scala class `=:=` is sealed and difficult/impossible to subclass, so we cannot create a mock which raises
// exceptions on use and test each method.
// - It's possible to compile code, decompile it, and inspect bytecode but this requires a hell of a lot of
// machinery. See github.com/scala/scala3/blob/main/compiler/test/dotty/tools/backend/jvm/ArrayApplyOptTest.scala
// as an example of what would be required. (it's a lot -- compiling to disk, finding the right class files,
// interpreting bytecode, ignoring irrelevant differences, etc.)
object newtypes:

@FunctionalInterface
trait SameRuntime[A, T]:
abstract class SameRuntime[A, T]:
// TODO: Convert in both directions...
def apply(a: A): T

extension (a: A) def transform: T = apply(a)

object SameRuntime:
def apply[A, T](f: A => T): SameRuntime[A, T] = new:
def apply(a: A): T = f(a)
override def apply(a: A): T = f(a)

type StringRuntime[A] = SameRuntime[A, String]
type IntRuntime[A] = SameRuntime[A, Int]
type LongRuntime[A] = SameRuntime[A, Long]
type FloatRuntime[A] = SameRuntime[A, Float]
type DoubleRuntime[A] = SameRuntime[A, Double]

trait TotalWrapper[Newtype, Impl](using ev: Newtype =:= Impl):
inline def raw(inline a: Newtype): Impl = a
inline def apply(inline s: Impl): Newtype = s.asInstanceOf[Newtype]
inline def from[M[_]](inline f: M[Impl]): M[Newtype] = f.asInstanceOf[M[Newtype]]
inline def from[M[_], B](using sr: SameRuntime[B, Impl])(inline f: M[B]): M[Newtype] =
abstract class TotalWrapper[Newtype, Impl](using Newtype =:= Impl):
inline final def raw(inline a: Newtype): Impl = a.asInstanceOf[Impl]
inline final def apply(inline s: Impl): Newtype = s.asInstanceOf[Newtype]
inline final def from[M[_]](inline f: M[Impl]): M[Newtype] = f.asInstanceOf[M[Newtype]]
inline final def from[M[_], B](using sr: SameRuntime[B, Impl])(inline f: M[B]): M[Newtype] =
f.asInstanceOf[M[Newtype]]
inline def from[M[_], B](inline other: TotalWrapper[B, Impl])(inline f: M[B]): M[Newtype] =
inline final def from[M[_], B](inline other: TotalWrapper[B, Impl])(inline f: M[B]): M[Newtype] =
f.asInstanceOf[M[Newtype]]
inline def raw[M[_]](inline f: M[Newtype]): M[Impl] = f.asInstanceOf[M[Impl]]

given SameRuntime[Newtype, Impl] = identity
given SameRuntime[Impl, Newtype] = _.asInstanceOf[Newtype]
given (using Eq[Impl]): Eq[Newtype] = Eq.by(_.value)

extension (a: Newtype)
inline def value: Impl = a
inline def into[X](inline other: TotalWrapper[X, Impl]): X = other.apply(a)
inline def map(inline f: Impl => Impl): Newtype = apply(f(a))
inline final def raw[M[_]](inline f: M[Newtype]): M[Impl] = f.asInstanceOf[M[Impl]]

given SameRuntime[Newtype, Impl] = raw(_)
given SameRuntime[Impl, Newtype] = apply(_)
// Avoiding a simple cast because Eq is @specialized, so there might be edge cases.
given (using eqi: Eq[Impl]): Eq[Newtype] = new:
override def eqv(x: Newtype, y: Newtype) = eqi.eqv(raw(x), raw(y))

extension (inline a: Newtype)
inline def value: Impl = raw(a)
inline def into[X](inline other: TotalWrapper[X, Impl]): X = other.apply(raw(a))
inline def map(inline f: Impl => Impl): Newtype = apply(f(raw(a)))
end TotalWrapper

trait FunctionWrapper[Newtype, Impl](using ev: Newtype =:= Impl) extends TotalWrapper[Newtype, Impl]:
extension (a: Newtype) inline def apply: Impl = a
abstract class FunctionWrapper[Newtype, Impl](using Newtype =:= Impl) extends TotalWrapper[Newtype, Impl]:
extension (inline a: Newtype) inline def apply: Impl = a.asInstanceOf[Impl]

trait OpaqueString[A](using A =:= String) extends TotalWrapper[A, String]:
abstract class OpaqueString[A](using A =:= String) extends TotalWrapper[A, String]:
given Show[A] = _.value
given Render[A] = _.value

trait OpaqueInt[A](using A =:= Int) extends TotalWrapper[A, Int]:
/// --- SIDE NOTE ---
/// Despite looking very similar to each other, the following classes are necessary to split out. Math
/// methods are overloaded and each class uses methods specific to its underlying type. It's possible
/// this could be condensed using @specialized, once it is implemented for scala3 / dotty.
/// -----------------

/** Use [[OpaqueIntSafer]] if possible. This class may be removed in the future as it has relaxed type
* safety.
*/
abstract class OpaqueInt[A](using A =:= Int) extends TotalWrapper[A, Int]:
extension (inline a: A)
inline def unary_- : A = apply(-raw(a))
inline infix def >(inline o: Int): Boolean = raw(a) > o
Expand All @@ -69,8 +105,9 @@ object newtypes:
inline infix def -(inline o: A): A = a - raw(o)
inline def atLeast(inline bot: A): A = atLeast(raw(bot))
inline def atMost(inline top: A): A = atMost(raw(top))
end OpaqueInt

trait OpaqueIntSafer[A](using A =:= Int) extends TotalWrapper[A, Int]:
abstract class OpaqueIntSafer[A](using A =:= Int) extends TotalWrapper[A, Int]:
extension (inline a: A)
inline def unary_- : A = apply(-raw(a))
inline infix def >(inline o: A): Boolean = raw(a) > raw(o)
Expand All @@ -81,41 +118,84 @@ object newtypes:
inline infix def -(inline o: A): A = apply(raw(a) - raw(o))
inline def atLeast(inline bot: A): A = apply(Math.max(raw(a), raw(bot)))
inline def atMost(inline top: A): A = apply(Math.min(raw(a), raw(top)))
end OpaqueIntSafer

trait OpaqueLong[A](using A =:= Long) extends TotalWrapper[A, Long]
trait OpaqueDouble[A](using A =:= Double) extends TotalWrapper[A, Double]:
extension (inline a: A) inline def +(inline o: Int): A = apply(raw(a) + o)
trait OpaqueFloat[A](using A =:= Float) extends TotalWrapper[A, Float]
abstract class OpaqueLong[A](using A =:= Long) extends TotalWrapper[A, Long]:
extension (inline a: A)
inline def unary_- : A = apply(-raw(a))
inline infix def >(inline o: A): Boolean = raw(a) > raw(o)
inline infix def <(inline o: A): Boolean = raw(a) < raw(o)
inline infix def >=(inline o: A): Boolean = raw(a) >= raw(o)
inline infix def <=(inline o: A): Boolean = raw(a) <= raw(o)
inline infix def +(inline o: A): A = apply(raw(a) + raw(o))
inline infix def -(inline o: A): A = apply(raw(a) - raw(o))
inline def atLeast(inline bot: A): A = apply(Math.max(raw(a), raw(bot)))
inline def atMost(inline top: A): A = apply(Math.min(raw(a), raw(top)))
end OpaqueLong

import scala.concurrent.duration.FiniteDuration
trait OpaqueDuration[A](using A =:= FiniteDuration) extends TotalWrapper[A, FiniteDuration]
abstract class OpaqueDouble[A](using A =:= Double) extends TotalWrapper[A, Double]:
extension (inline a: A)
inline def unary_- : A = apply(-raw(a))
inline infix def >(inline o: A): Boolean = raw(a) > raw(o)
inline infix def <(inline o: A): Boolean = raw(a) < raw(o)
inline infix def >=(inline o: A): Boolean = raw(a) >= raw(o)
inline infix def <=(inline o: A): Boolean = raw(a) <= raw(o)
inline infix def +(inline o: A): A = apply(raw(a) + raw(o))
inline infix def -(inline o: A): A = apply(raw(a) - raw(o))
inline def atLeast(inline bot: A): A = apply(Math.max(raw(a), raw(bot)))
inline def atMost(inline top: A): A = apply(Math.min(raw(a), raw(top)))

abstract class YesNo[A](using ev: Boolean =:= A):
val Yes: A = true
val No: A = false
@deprecated("Unsafe and be removed later.", "11.3.0")
inline def +(inline o: Int): A = apply(raw(a) + o)
end OpaqueDouble

inline def from[M[_]](inline a: M[Boolean]): M[A] = a.asInstanceOf[M[A]]
abstract class OpaqueFloat[A](using A =:= Float) extends TotalWrapper[A, Float]:
extension (inline a: A)
inline def unary_- : A = apply(-raw(a))
inline infix def >(inline o: A): Boolean = raw(a) > raw(o)
inline infix def <(inline o: A): Boolean = raw(a) < raw(o)
inline infix def >=(inline o: A): Boolean = raw(a) >= raw(o)
inline infix def <=(inline o: A): Boolean = raw(a) <= raw(o)
inline infix def +(inline o: A): A = apply(raw(a) + raw(o))
inline infix def -(inline o: A): A = apply(raw(a) - raw(o))
inline def atLeast(inline bot: A): A = apply(Math.max(raw(a), raw(bot)))
inline def atMost(inline top: A): A = apply(Math.min(raw(a), raw(top)))
end OpaqueFloat

given SameRuntime[A, Boolean] = _ == Yes
given SameRuntime[Boolean, A] = if _ then Yes else No
given Eq[A] = Eq.by(_.value)
import scala.concurrent.duration.FiniteDuration
abstract class OpaqueDuration[A](using A =:= FiniteDuration) extends TotalWrapper[A, FiniteDuration]:
extension (inline a: A)
inline def unary_- : A = apply(-raw(a))
inline infix def >(inline o: A): Boolean = raw(a) > raw(o)
inline infix def <(inline o: A): Boolean = raw(a) < raw(o)
inline infix def >=(inline o: A): Boolean = raw(a) >= raw(o)
inline infix def <=(inline o: A): Boolean = raw(a) <= raw(o)
inline infix def +(inline o: A): A = apply(raw(a) + raw(o))
inline infix def -(inline o: A): A = apply(raw(a) - raw(o))
inline def atLeast(inline bot: A): A = apply(raw(a).max(raw(bot)))
inline def atMost(inline top: A): A = apply(raw(a).min(raw(top)))
end OpaqueDuration

inline def apply(inline b: Boolean): A = b
abstract class YesNo[A](using A =:= Boolean) extends TotalWrapper[A, Boolean]:
final val Yes: A = apply(true)
final val No: A = apply(false)

extension (inline a: A)
inline def value: Boolean = a == Yes
inline def flip: A = if value then No else Yes
inline def yes: Boolean = value
inline def no: Boolean = !value
inline def &&(inline other: A) = a.value && other.value
inline def `||`(inline other: A) = a.value || other.value
inline def flip: A = apply(!raw(a))
inline def unary_! : A = a.flip
inline def yes: Boolean = raw(a)
inline def no: Boolean = !raw(a)
inline def &&(inline other: A): A = apply(raw(a) && raw(other))
inline def `||`(inline other: A): A = apply(raw(a) || raw(other))
end YesNo

inline def sameOrdering[A, T](using bts: SameRuntime[T, A], ord: Ordering[A]): Ordering[T] =
Ordering.by(bts.apply(_))

inline def stringOrdering[T: StringRuntime](using Ordering[String]): Ordering[T] = sameOrdering[String, T]
inline def intOrdering[T: IntRuntime](using Ordering[Int]): Ordering[T] = sameOrdering[Int, T]
inline def longOrdering[T: LongRuntime](using Ordering[Long]): Ordering[T] = sameOrdering[Long, T]
inline def floatOrdering[T: FloatRuntime](using Ordering[Float]): Ordering[T] = sameOrdering[Float, T]
inline def doubleOrdering[T: DoubleRuntime](using Ordering[Double]): Ordering[T] = sameOrdering[Double, T]

given [A](using sr: SameRuntime[Boolean, A]): Zero[A] = Zero(sr(false))

0 comments on commit 9f63ff6

Please sign in to comment.