2

The task is to perform transformation from string value to Int or BigDecimal, depends on string value length.

For example, if stringValue > 10 => stringValue.toInt, else => BigDecimal(stringValue)

I'm trying to use Type Class:

 trait Number
 case class IntNumber(ch: String) extends Number
 case class BigDecNumber(ch: String) extends Number

 def wrap(ch: String): Number = ch match {
     case c if (c.length <= 10) => IntNumber(c)
     case c if (c.length > 10) => BigDecNumber(c)
 }

 trait TypeTransformer[A <: Number, B] {
      def toDigit(ch: A): B
 }

object transformer {
      def transform[A <: Number, B](in: A)(implicit t: TypeTransformer[A, B]): B = t.toDigit(in)

      implicit val stringToInt: TypeTransformer[IntNumber, Int] = (number: IntNumber) => number.ch.toInt
      implicit val stringToBigDecimal: TypeTransformer[BigDecNumber, BigDecimal] = (number: BigDecNumber) => BigDecimal(number.ch)
 }

But, when I'm trying to apply this transformation:

transformer.transform(wrap("1231547234133123"))

I see the compilation error: No implicits found for parameter TypeTransformer[Number, B_]

What am I doing wrong?

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
Jelly
  • 972
  • 1
  • 17
  • 40
  • 2
    Because there isn't any `transformer` for `Number` only for its cases, and since you won't know which case it will be until runtime then you can't resolve the correct type at compile-time. – Luis Miguel Mejía Suárez Mar 09 '23 at 13:40
  • Luis Miguel Mejía Suárez, should I create additional implicit method, for example: implicit def numberTransform(ch: String) = new NumberWrapper(ch) , where class NumberWrapper implements transformation String to IntNumber or BigDecNumber ? – Jelly Mar 09 '23 at 15:09
  • 1
    This could be solved using path-dependent types, but it would complicate the implementation, so I wouldn't recommend that way if you aren't very familiar with how they work. – Mateusz Kubuszok Mar 09 '23 at 15:15
  • I would make this in a more straightforward way. Just make the member of the ADT to hold the transformed number rather than the raw string, then make a `fromString` factory in the companion object of `Number` – Luis Miguel Mejía Suárez Mar 09 '23 at 15:15

1 Answers1

4

Type-class instances (implicits) are resolved at compile time. So you have to know at compile time whether a number is IntNumber or BigDecNumber, whether c.length <= 10 or c.length > 10.

Runtime vs. Compile time

Type classes is kind of pattern matching at compile time.

If you know only at runtime whether c.length <= 10 or c.length > 10 then you should better use ordinary pattern matching

object transformer {
  def transform(in: Number): Any = in match {
    case num: IntNumber    => num.ch.toInt
    case num: BigDecNumber => BigDecimal(num.ch)
  }
}

transformer.transform(wrap("1231547234133123")) // 1231547234133123

In Scala 3 you can have union type as the return type

https://docs.scala-lang.org/scala3/reference/new-types/union-types.html (Scala 3)

How to define "type disjunction" (union types)? (Scala 2)

object transformer:
  def transform(in: Number): Int | BigDecimal = in match
    case num: IntNumber    => num.ch.toInt
    case num: BigDecNumber => BigDecimal(num.ch)

Alternatively you can define an instance of the type class for parent type Number

object transformer {
  def transform[A <: Number, B](in: A)(implicit t: TypeTransformer[A, B]): B = t.toDigit(in)

  implicit val stringToInt: TypeTransformer[IntNumber, Int] = (number: IntNumber) => number.ch.toInt
  implicit val stringToBigDecimal: TypeTransformer[BigDecNumber, BigDecimal] = (number: BigDecNumber) => BigDecimal(number.ch)
  implicit val stringToAny: TypeTransformer[Number, Any /* Int | BigDecimal */] = {
    case num: IntNumber    => num.ch.toInt
    case num: BigDecNumber => BigDecimal(num.ch)
  }
}

import transformer._
transformer.transform(wrap("1231547234133123")) // 1231547234133123

By the way, you should better put type-class instances into companion object, then you'll not have to import them.

You can express the instance TypeTransformer[Number, Any] (or TypeTransformer[Number, Int | BigDecimal]) via TypeTransformer[IntNumber, Int] and TypeTransformer[BigDecNumber, BigDecimal]

implicit val stringToAny: TypeTransformer[Number, Any /* Int | BigDecimal */] = {
  case num: IntNumber    => implicitly[TypeTransformer[IntNumber, Int]].toDigit(num)
  case num: BigDecNumber => implicitly[TypeTransformer[BigDecNumber, BigDecimal]].toDigit(num)
}

or derive it if the trait is sealed (to avoid code duplication for example if there are many inheritors of the trait) for example with Shapeless

sealed trait Number
case class IntNumber(ch: String) extends Number
case class BigDecNumber(ch: String) extends Number

// libraryDependencies += "com.chuusai" %% "shapeless" % "2.3.10"
import shapeless.{:+:, CNil, Coproduct, Generic}

trait TypeTransformer[A /*<: Number*/, B] {
  def toDigit(ch: A): B
}
object TypeTransformer  {
  def transform[A, B](in: A)(implicit t: TypeTransformer[A, B]): B = t.toDigit(in)

  implicit val stringToInt: TypeTransformer[IntNumber, Int] = (number: IntNumber) => number.ch.toInt
  implicit val stringToBigDecimal: TypeTransformer[BigDecNumber, BigDecimal] = (number: BigDecNumber) => BigDecimal(number.ch)

  implicit def gen[A, C <: Coproduct, Out](implicit
    generic: Generic.Aux[A, C],
    typeTransformer: TypeTransformer[C, Out]
  ): TypeTransformer[A, Out] =
    a => typeTransformer.toDigit(generic.to(a))

  implicit def coprod[H, T <: Coproduct, OutH <: Out, OutT <: Out, Out](implicit
    headTypeTransformer: TypeTransformer[H, OutH],
    tailTypeTransformer: TypeTransformer[T, OutT]
  ): TypeTransformer[H :+: T, Out] =
    _.eliminate(headTypeTransformer.toDigit, tailTypeTransformer.toDigit)

  implicit def single[H, Out](implicit
    headTypeTransformer: TypeTransformer[H, Out]
  ): TypeTransformer[H :+: CNil, Out] =
    _.eliminate(headTypeTransformer.toDigit, _.impossible)
}

TypeTransformer.transform[Number, Any](wrap("1231547234133123")) //1231547234133123

Scala how to derivate a type class on a trait

Use the lowest subtype in a typeclass?

Type class instance for case objects defined in sealed trait

Or if you really know at compile time that c.length <= 10 or c.length > 10 then you can make wrap a type class too (with macros or with singleton-ops)

// libraryDependencies += "eu.timepit" %% "singleton-ops" % "0.5.0"
import singleton.ops.{<=, >, Length, Require}

trait Wrap[S <: String with Singleton, Out <: Number] {
  def toNumber(s: S): Out
}
object Wrap {
  implicit def less10[S <: String with Singleton](implicit
    require: Require[Length[S] <= 10]
  ): Wrap[S, IntNumber] = IntNumber(_)
  
  implicit def more10[S <: String with Singleton](implicit
    require: Require[Length[S] > 10]
  ): Wrap[S, BigDecNumber] = BigDecNumber(_)
}

def wrap[S <: String with Singleton, Out <: Number](s: S)(implicit w: Wrap[S, Out]): Out = w.toNumber(s)

trait TypeTransformer[A <: Number, B] {
  def toDigit(ch: A): B
}
object TypeTransformer  {
  def transform[A <: Number, B](in: A)(implicit t: TypeTransformer[A, B]): B = t.toDigit(in)

  implicit val stringToInt: TypeTransformer[IntNumber, Int] = (number: IntNumber) => number.ch.toInt
  implicit val stringToBigDecimal: TypeTransformer[BigDecNumber, BigDecimal] = (number: BigDecNumber) => BigDecimal(number.ch)
}

TypeTransformer.transform(wrap("1231547234133123")) // 1231547234133123

val num = wrap("1231547234133123")
num: BigDecNumber // compiles
TypeTransformer.transform(num) // 1231547234133123

val num1: Number = wrap("1231547234133123")
TypeTransformer.transform(num1) // doesn't compile

In Scala 3 you can use transparent inline methods, compile-time operations, and match types

import scala.compiletime.summonFrom
import scala.compiletime.ops.string.Length
import scala.compiletime.ops.int.<=

transparent inline def wrap[S <: String with Singleton](s: S): Number = summonFrom {
  case _: (Length[S] <= 10) => IntNumber(s)
  case _                    => BigDecNumber(s)
}

type TypeTransformer[A <: Number] = A match
  case IntNumber    => Int
  case BigDecNumber => BigDecimal

def transform[A <: Number](a: A): TypeTransformer[A] = a match
  case num: IntNumber    => num.ch.toInt
  case num: BigDecNumber => BigDecimal(num.ch)

transform(wrap("1231547234133123")) // 1231547234133123

Sometimes you really have to resolve type class instances at runtime. In such case you can use runtime compilation (with reflective toolbox)

def wrap(ch: String): Number = ch match {
  case c if (c.length <= 10) => IntNumber(c)
  case c if (c.length > 10)  => BigDecNumber(c)
}

trait TypeTransformer[A <: Number, B] {
  def toDigit(ch: A): B
}
object TypeTransformer  {
  def transform[A <: Number, B](in: A)(implicit t: TypeTransformer[A, B]): B = t.toDigit(in)

  implicit val stringToInt: TypeTransformer[IntNumber, Int] = (number: IntNumber) => number.ch.toInt
  implicit val stringToBigDecimal: TypeTransformer[BigDecNumber, BigDecimal] = (number: BigDecNumber) => BigDecimal(number.ch)
}

import scala.reflect.runtime
import runtime.universe._
import scala.tools.reflect.ToolBox // libraryDependencies += scalaOrganization.value % "scala-compiler" % scalaVersion.value

val rm = runtime.currentMirror
val tb = rm.mkToolBox()

val num: Number = wrap("1231547234133123")
num.getClass // class BigDecNumber

TypeTransformer.transform(num)(
  tb.eval(
    tb.untypecheck(
      tb.inferImplicitValue(
        appliedType(
          typeOf[TypeTransformer[_, _]].typeConstructor,
          rm.classSymbol(num.getClass).toType,
          WildcardType
        ),
        silent = false
      )
    )
  ).asInstanceOf[TypeTransformer[Number, _]]
) // 1231547234133123
TypeTransformer.transform(num)(
  tb.eval(
    q"implicitly[TypeTransformer[${rm.classSymbol(num.getClass)}, _]]"
  ).asInstanceOf[TypeTransformer[Number, _]]
) // 1231547234133123

By the way, you could also make the return type B a type member of the type class rather than type parameter (for t: TypeTransformer[A], t.B is a path-dependent type) and use one of the solutions above

trait TypeTransformer[A <: Number] {
  type B
  def toDigit(ch: A): B
}
object TypeTransformer {
  type Aux[A <: Number, _B] = TypeTransformer[A] { type B = _B }
  def instance[A <: Number, _B](f: A => _B): Aux[A, _B] = new TypeTransformer[A] { 
    override type B = _B
    override def toDigit(ch: A): B = f(ch)
  }
    
  def transform[A <: Number](in: A)(implicit t: TypeTransformer[A]): t.B = t.toDigit(in)

  implicit val stringToInt: Aux[IntNumber, Int] = instance(_.ch.toInt)
  implicit val stringToBigDecimal: Aux[BigDecNumber, BigDecimal] = instance(num => BigDecimal(num.ch))

  // for example the solution with adding implicit for Number
  implicit val stringToAny: Aux[Number, Any /* Int | BigDecimal */] = instance {
    case num: IntNumber    => implicitly[TypeTransformer[IntNumber]].toDigit(num)
    case num: BigDecNumber => implicitly[TypeTransformer[BigDecNumber]].toDigit(num)
  }
}

When are dependent types needed in Shapeless?

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • Dmytro Mitin, I tried to apply path-dependent type way, which have been shown by you at the end of your great answer. For example (TypeTransformer.transform(wrap("224534"))). And receive error: No implicits found for parameter TypeTransformer[Number]. To treat it, I used the following implicit: implicit val stringToAny: Aux[Number, (Int ∨ BigDecimal)], where union type I defined as here: https://milessabin.com/blog/2011/06/09/scala-union-types-curry-howard/ It compiled fine, but the returned type is "Int ∨ BigDecimal". I expected, the returned type will be Int or BigDecimal – Jelly Mar 11 '23 at 21:49
  • 1
    @Jelly Well, I didn't mean that making `B` a type member rather than type parameter is a solution itself. You should use this with one of the solutions above, e.g. defining implicit for `Number`: https://scastie.scala-lang.org/DmytroMitin/e4mWSMNvQAWN8RsfisksdQ (Scala 2) https://scastie.scala-lang.org/DmytroMitin/qB3F2CY6S2KvBw45Q6KDNg/1 (Scala 3) I meant that when you have incoming and outcoming types it's normally convenient to make the former type parameters and the latter type members. Then you can specify all the former and expect that the latter will be inferred. – Dmytro Mitin Mar 12 '23 at 15:42
  • 1
    @Jelly `shapeless.∨` is an emulation of union types in Scala 2, it's not actual replacement for Scala 3 union types (which do not exist in Scala 2). `shapeless.∨` can be used in context bounds, not return types: https://stackoverflow.com/questions/73916845/does-shapeless-for-scala-2-has-analogue-for-scala-3-union-types – Dmytro Mitin Mar 12 '23 at 15:49