Class, trait, impl in different *.scala files?

(new Scala user here, apologies if using incorrect terms)

Suppose we have class C, trait T, and the implementation of C satisfying T.

In Rust, these can be in three separate files:
c.rs = define class C
t.rs = define trait T
foo.rs = implementation of C satisfying T

In all the examples of Scala code I have seen so far, the patter is:
t.scala (or c.scala) = define trait T
c.scala = define class C, and provide implementation at the same time
(here c.scala and t.scala) may be the same file

In practice, this ends up with code of the form:

sealed trait T {
...
}

case class C1(..) extends T { ... }
case class C2(..) extends T { ... }
case class C3(..) extends T { ... }
case class C4(..) extends T { ... }

The one thing I do not like about this style is that because all the implementations are provided inline, it is sometimes hard to see “what all the constructors for this Enum” ?

==============================

Question: In Scala, is there a way to separate out the implementation of C satisfying T from the definition of the fields of C ?

To expand on this a bit more, I am trying to figure out if there is a way to separate out:

  1. defining the fields of a class from
  2. the traits the class extends / their implementations

In Rust, I can do something like:

struct Foo { ... }

then, within the same crate, in other files, do:

impl TraitOne for Foo { ... }
impl TraitTwo for Foo { ... }

====

In Scala, in all the sample code I have seen so far, we have to do:


class Foo (...) extends ... with TraitOne with TraitTwo {
}

where, as a result, we have to simultaneously define: (1) the fields of the class, (2) all traits the class satisfies, and (3) the implementations of the traits

My question is whether we can separate (1) from (2/3)

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 :slight_smile:

1 Like

You can also use self types to separate the implementation code into different files:

//T.scala
package foo

sealed trait T {
  ...
}

case class C1(..) extends T with C1Impl
case class C2(..) extends T with C2Impl
case class C3(..) extends T with C3Impl
case class C4(..) extends T with C4Impl
//C1Impl.scala
package foo

private[foo] trait C1Impl { this: C1 => 
  // lots of implementation code
}
3 Likes