Yes, you can, by doing the same that in Rust, using a typeclass for ad-hoc Polymorphism instead of using an interface for classic subtyping Polymorphism (in the OOP form of subclassing).
But first, we need to separate your code in two parts.
A sealed trait
is usually used to define another pattern, algebraic data types (ADTs); which is basically a way to define a type which represents a known group of states.
For example, an Option
is either a value (Some
) or not (None
), a List
is either an empty list (Nil
) or an element and another list (::
), or an Either
is either (bad pun intended) some value (Left
) or another (Right
); etc.
For an ADT, Scala requires you to define a sealed trait
and all its case classes / objects
in the same file.
But, outside of ADTs, you can use the typeclass pattern to split the definition of an interface, the type and the implementation in three parts; which can be on different files (although many people argue that it is better to group either the second with the third or the first with third in the same file for ease of maintainability).
Also, usually for an ADT, you do not implement a typeclass for each component, but rather for the whole ADT (typically using pattern matching).
Anyways, so a simple example would be the following:
// The typeclass itself.
// file: Show.scala
trait Show[A] {
def show(a: A): String
}
object Show {
// Summoner.
def apply[A](implicit show: Show[A]): Show[A] = show
// Common utility using the typeclass
def print[A : Show](value: A): Unit = {
println(Show[A].show(value))
}
// Implementation for common types.
implicit final val ShowString: Show[String] =
new Show[String] {
override def show(s: String): String =
s
}
implicit final val ShowInt: Show[Int] =
new Show[Int] {
override def show(i: Int): String =
i.toString
}
implicit final val ShowDouble: Show[Double] =
new Show[Double] {
override def show(d: Double): String =
f"$d%1.df"
}
// Typeclass derivation.
implicit def ShowList[A](implicit showA: Show[A]): Show[List[A]] =
new Show[List[A]] {
override def show(list: List[A]): String =
list.map(a => showA.show(a)).mkString("[", ",", "]")
}
}
// Some type (ADT)
// file: Result.scala
sealed trait Result[+A]
final case class Success[+A](value: A) extends Result[A]
final case class Error(msg: String) extends Result[Nothing]
// The implementation of the typeclass for the type.
// file ResultShow.scala
object ResultShow {
// Usually this would be in the companion object of Result
implicit final def instance[A](implicit showA: Show[A]) Show[Result[A]] =
new Show[List[A]] {
override def show(result: Result[A]): String =
result match {
case Success(a) => showA.show(a)
case Error(msg) => Show[String].show(s"Error: ${msg}")
}
}
}
// Usage
// file: Main.scala
object Main {
def main(args: Array[String]): Unit = {
val r1: Result[List[Double] = Success(List(0.0d, 1.5d, 3.3d))
val r1: Result[List[Double] = Error(msg = "Kabom")
Show.print(r1)
Show.print(r2)
}
}
Also, if you prefer a more method-like syntax, you can get that using extension methods.
// Extension methods
// file: ShowSyntax.scala
object syntax {
object show {
// Value class to avoid allocation.
implicit class ShowOps[A](private val a: A) extends AnyVal {
@inline final def show(implicit showA: Show[A]): String =
showA.show(a)
}
}
}
// Use the syntax.
// file: Whatever.scala
import syntax.show._ // Put the syntax in scope; we do not care about the name of the implicit class.
val r = Success(10)
println(r.show)
This approach is common in FP-based libraries like cats.
If you want a more detailed explanation and comparison of this pattern with other forms of polymorphism in Scala, please take a look to this.
Also, please do not hesitate to ask any question 