英文:
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 <= 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?
答案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 <= 10
or c.length > 10
.
Type classes are a form of pattern matching at compile time.
If you only know at runtime whether c.length <= 10
or c.length > 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 <= 10
or c.length > 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 <= 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)
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 => 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
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/
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)
}
}
https://stackoverflow.com/questions/51131067/when-are-dependent-types-needed-in-shapeless
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论