É possível usar uma typeclass para implementar uma característica?
Eu tenho uma situação em que gostaria de implementar uma determinada característica ( CanBeStringno exemplo abaixo). Eu gostaria de ter a opção de implementar essa característica usando uma classe de caso recém-criada ( NewImplementationno exemplo abaixo), ou implementá-la adicionando funcionalidade a algum tipo pré-existente (apenas Intno exemplo abaixo), usando um tipo classe. Isso provavelmente é mais bem ilustrado pelo seguinte:
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.
}
É possível algo assim? Só recentemente estou descobrindo o tópico de typeclasses, então estou ciente de que o que estou pedindo aqui pode ser possível, mas apenas imprudente - se for o caso, entre em contato e me diga qual pode ser um idioma melhor.
Agradecemos antecipadamente e também depois!
Respostas
Por exemplo, você pode ter duas versões sobrecarregadas de testAsFunc(estilo OOP e estilo typeclass)
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
}
Ou se você preferir ter o único, testAsFuncentão você pode adicionar instâncias da classe de tipo para subtipos do traço a serem implementados
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
}
Observe que, se para um chouver ambos os c.asStringmétodos OOP e extensão c.asString, apenas o primeiro será realmente chamado.