È possibile utilizzare una typeclass per implementare un tratto?
Ho una situazione in cui vorrei implementare un determinato tratto ( CanBeString
nell'esempio sotto). Vorrei avere la possibilità di implementare quel tratto usando una classe case appena creata ( NewImplementation
nell'esempio sotto), o di implementarla aggiungendo funzionalità a qualche tipo preesistente (solo Int
nell'esempio sotto), usando un tipo classe. Questo è probabilmente meglio illustrato da quanto segue:
package example
// typeclass
trait ConvertsToString[A] {
def asString(value: A): String
}
// the trait I would like the typeclass to implement
trait CanBeString {
def asString: String
}
// this implementation approach taken from the scala with cats book
object ConvertsToStringInstances {
implicit val intConvertsToString: ConvertsToString[Int] =
new ConvertsToString[Int] {
def asString(value: Int): String = s"${value}"
}
}
object ConvertsToStringSyntax {
implicit class ConvertsToStringOps[A](value: A) {
def asString(implicit c: ConvertsToString[A]): String = c.asString(value)
}
}
object Test {
import ConvertsToStringInstances._
import ConvertsToStringSyntax._
def testAsFunc(c: CanBeString): String = c.asString
case class NewImplementation (f: Double) extends CanBeString {
def asString = s"{f}"
}
println(testAsFunc(NewImplementation(1.002))) // this works fine!
println(testAsFunc(1)) // this sadly does not.
}
È possibile qualcosa di simile? Sto scoprendo solo di recente l'argomento delle classi tipografiche, quindi sono consapevole che ciò che sto chiedendo qui potrebbe essere possibile ma poco saggio - in tal caso, per favore intervieni e fammi sapere quale potrebbe essere un idioma migliore.
Grazie in anticipo, e anche dopo!
Risposte
Ad esempio, puoi avere due versioni sovraccaricate di testAsFunc
(stile OOP e stile classe di tipo)
object Test {
...
def testAsFunc(c: CanBeString): String = c.asString
def testAsFunc[C: ConvertsToString](c: C): String = c.asString
println(testAsFunc(NewImplementation(1.002))) // {f}
println(testAsFunc(1)) // 1
}
Oppure, se preferisci avere l'unico testAsFunc
, puoi aggiungere istanze della classe del tipo per i sottotipi del tratto da implementare
object ConvertsToStringInstances {
implicit val intConvertsToString: ConvertsToString[Int] = ...
implicit def canBeStringSubtypeConvertsToString[A <: CanBeString]: ConvertsToString[A] =
new ConvertsToString[A] {
override def asString(value: A): String = value.asString
}
}
object Test {
...
def testAsFunc[C: ConvertsToString](c: C): String = c.asString
println(testAsFunc(NewImplementation(1.002))) // {f}
println(testAsFunc(1)) // 1
}
Si noti che se per a c
ci sono sia OOP-ish c.asString
che extension-method, c.asString
allora viene effettivamente chiamato solo il primo.