Scala: 具有值条件的隐式类型转换

huangapple go评论62阅读模式
英文:

Scala: implicit type transformation with condition on value

问题

代码部分不翻译。以下是翻译好的部分:

"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?"

英文:

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 &lt;= 10) =&gt; IntNumber(c)
     case c if (c.length &gt; 10) =&gt; BigDecNumber(c)
 }

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

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

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

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

transformer.transform(wrap(&quot;1231547234133123&quot;))

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

What am I doing wrong?

答案1

得分: 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 &lt;= 10 or c.length &gt; 10.

Type classes are a form of pattern matching at compile time.

If you only know at runtime whether c.length &lt;= 10 or c.length &gt; 10, you should use ordinary pattern matching.

In Scala 3, you can use union types as the return type.

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

By the way, you should put type-class instances into a companion object to avoid the need for imports.

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

Sometimes you may need to resolve type class instances at runtime, which can be done using reflective toolbox.

You could also make the return type B a type member of the type class rather than a type parameter.

Source: https://stackoverflow.com/questions/846103/runtime-vs-compile-time

英文:

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 &lt;= 10 or c.length &gt; 10.

https://stackoverflow.com/questions/846103/runtime-vs-compile-time

Type classes is kind of pattern matching at compile time.

If you know only at runtime whether c.length &lt;= 10 or c.length &gt; 10 then you should better use ordinary pattern matching

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

transformer.transform(wrap(&quot;1231547234133123&quot;)) // 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)

https://stackoverflow.com/questions/3508077/how-to-define-type-disjunction-union-types (Scala 2)

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

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

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

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

import transformer._
transformer.transform(wrap(&quot;1231547234133123&quot;)) // 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    =&gt; implicitly[TypeTransformer[IntNumber, Int]].toDigit(num)
  case num: BigDecNumber =&gt; 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 += &quot;com.chuusai&quot; %% &quot;shapeless&quot; % &quot;2.3.10&quot;
import shapeless.{:+:, CNil, Coproduct, Generic}

trait TypeTransformer[A /*&lt;: 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) =&gt; number.ch.toInt
  implicit val stringToBigDecimal: TypeTransformer[BigDecNumber, BigDecimal] = (number: BigDecNumber) =&gt; BigDecimal(number.ch)

  implicit def gen[A, C &lt;: Coproduct, Out](implicit
    generic: Generic.Aux[A, C],
    typeTransformer: TypeTransformer[C, Out]
  ): TypeTransformer[A, Out] =
    a =&gt; typeTransformer.toDigit(generic.to(a))

  implicit def coprod[H, T &lt;: Coproduct, OutH &lt;: Out, OutT &lt;: 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(&quot;1231547234133123&quot;)) //1231547234133123

https://stackoverflow.com/questions/75417002/scala-how-to-derivate-a-type-class-on-a-trait

https://stackoverflow.com/questions/61392874/use-the-lowest-subtype-in-a-typeclass/

https://stackoverflow.com/questions/73990571/type-class-instance-for-case-objects-defined-in-sealed-trait

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

// libraryDependencies += &quot;eu.timepit&quot; %% &quot;singleton-ops&quot; % &quot;0.5.0&quot;
import singleton.ops.{&lt;=, &gt;, Length, Require}

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

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

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

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

TypeTransformer.transform(wrap(&quot;1231547234133123&quot;)) // 1231547234133123

val num = wrap(&quot;1231547234133123&quot;)
num: BigDecNumber // compiles
TypeTransformer.transform(num) // 1231547234133123

val num1: Number = wrap(&quot;1231547234133123&quot;)
TypeTransformer.transform(num1) // doesn&#39;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.&lt;=

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

type TypeTransformer[A &lt;: Number] = A match
  case IntNumber    =&gt; Int
  case BigDecNumber =&gt; BigDecimal

def transform[A &lt;: Number](a: A): TypeTransformer[A] = a match
  case num: IntNumber    =&gt; num.ch.toInt
  case num: BigDecNumber =&gt; BigDecimal(num.ch)

transform(wrap(&quot;1231547234133123&quot;)) // 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 &lt;= 10) =&gt; IntNumber(c)
  case c if (c.length &gt; 10)  =&gt; BigDecNumber(c)
}

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

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

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

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

val num: Number = wrap(&quot;1231547234133123&quot;)
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&quot;implicitly[TypeTransformer[${rm.classSymbol(num.getClass)}, _]]&quot;
  ).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 &lt;: Number] {
  type B
  def toDigit(ch: A): B
}
object TypeTransformer {
  type Aux[A &lt;: Number, _B] = TypeTransformer[A] { type B = _B }
  def instance[A &lt;: Number, _B](f: A =&gt; _B): Aux[A, _B] = new TypeTransformer[A] { 
    override type B = _B
    override def toDigit(ch: A): B = f(ch)
  }
    
  def transform[A &lt;: 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 =&gt; BigDecimal(num.ch))

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

https://stackoverflow.com/questions/51131067/when-are-dependent-types-needed-in-shapeless

huangapple
  • 本文由 发表于 2023年3月9日 21:37:34
  • 转载请务必保留本文链接:https://go.coder-hub.com/75685358.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定