Трейты

Если провести аналогию с Java, то Scala trait похож на интерфейс в Java 8+.

trait-ы могут содержать:

В базовом использовании trait может использоваться как интерфейс, определяющий только абстрактные члены, которые будут реализованы другими классами:

trait Employee:
  def id: Int
  def firstName: String
  def lastName: String 

traits также могут содержать определенные методы:

trait HasLegs:
  def numLegs: Int
  def walk(): Unit
  def stop() = println("Stopped walking")
trait HasTail:
  def tailColor: String
  def wagTail() = println("Tail is wagging")
  def stopTail() = println("Tail is stopped")

Классы и объекты могут расширять несколько traits, что позволяет с их помощью создавать небольшие модули.

class IrishSetter(name: String) extends HasLegs, HasTail:
  val numLegs = 4
  val tailColor = "Red"
  def walk() = println("I’m walking")
  override def toString = s"$name is a Dog"

В классе IrishSetter реализованы все абстрактные параметры и методы, поэтому можно создать его экземпляр:

val d = IrishSetter("Big Red")
// d: IrishSetter = Big Red is a Dog

Класс также может переопределять методы trait-ов при необходимости.

Аргументы trait оцениваются непосредственно перед его инициализацией.

Особенности при расширении trait с параметрами конструктора

Одна потенциальная проблема с параметрами trait заключается в том, как предотвратить двусмысленность. Например, можно попробовать расширить Greeting дважды с разными параметрами:

trait Greeting(val name: String):
  def msg = s"How are you, $name"

class C extends Greeting("Bob"):
  println(msg)
class D extends C, Greeting("Bill") 
// error:
// trait Greeting is already implemented by superclass C,
// its constructor cannot be called again 

На самом деле эта программа не скомпилируется, потому что она нарушает второе правило для параметров trait:

  1. Если класс C расширяет параметризованный trait T, а его суперкласс — нет, то C должен передать аргументы в T.
  2. Если класс C расширяет параметризованный trait T и его суперкласс тоже, то C не должен передавать аргументы в T.
  3. trait-ы никогда не должны передавать аргументы родительским trait-ам.

Вот трейт, расширяющий параметризованный трейт Greeting.

trait FormalGreeting extends Greeting:
  override def msg = s"How do you do, $name"

Правильный способ создания класса E, расширяющего оба - Greeting и FormalGreeting (в любом порядке) - такой:

class E extends Greeting("Bob"), FormalGreeting
(new C).msg
// How are you, Bob
// res1: String = "How are you, Bob"
(new E).msg
// res2: String = "How do you do, Bob"

Trait-ы с параметрами контекста

Правило "требуется явное расширение" ослабляется, если отсутствующий trait содержит только параметры контекста. В этом случае ссылка на трейт неявно вставляется как дополнительный родитель с выводимыми аргументами. Например, вот вариант Greeting, где адресат является параметром контекста типа ImpliedName:

case class ImpliedName(name: String):
  override def toString = name

trait ImpliedGreeting(using val iname: ImpliedName):
  def msg = s"How are you, $iname"

trait ImpliedFormalGreeting extends ImpliedGreeting:
  override def msg = s"How do you do, $iname"

class F(using iname: ImpliedName) extends ImpliedFormalGreeting

Определение F в последней строке неявно расширяется до

class F(using iname: ImpliedName) extends
  Object,
  ImpliedGreeting(using iname),
  ImpliedFormalGreeting(using iname)

Обратите внимание на вставленную ссылку на ImpliedFormalGreeting - родительский trait ImpliedGreeting, которая не упоминалась явно.

given ImpliedName = ImpliedName("Bob")
(new F).msg
// res4: String = "How do you do, Bob"

Transparent traits

Trait-ы используются в двух случаях:

Некоторые trait-ы используются преимущественно в первой роли, и обычно их нежелательно видеть в выводимых типах. Примером может служить trait Product, который компилятор добавляет в качестве примеси к каждому case class-у или case object-у. В Scala 2 этот родительский trait иногда делает выводимые типы более сложными, чем они должны быть. Пример:

trait Kind
case object Var extends Kind
case object Val extends Kind
val x = Set(if false then Val else Var)

Здесь предполагаемый тип x равен Set[Kind & Product & Serializable], тогда как можно было бы надеяться, что это будет Set[Kind]. Основания для выделения именно этого типа следующие:

В примере - это тип Kind & Product & Serializable, так как все три trait-а являются trait-ами обоих Val и Var. Таким образом, этот тип становится предполагаемым типом элемента набора.

Scala 3 позволяет помечать trait примеси как transparent, что означает, что он может быть подавлен при выводе типа. Вот пример, который повторяет строки приведенного выше кода, но теперь с новым transparent trait S вместо Product:

transparent trait S
trait Kind
object Var extends Kind, S
object Val extends Kind, S
val x = Set(if true then Val else Var)
// x: Set[Kind] = ...

Теперь x предположил тип Set[Kind]. Общий transparent trait S не появляется в выводимом типе.

Прозрачные trait-ы

Trait-ы scala.Product, java.io.Serializable и java.lang.Comparable автоматически считаются transparent. Другие трейты превращаются в transparent trait с помощью модификатора transparent.

Как правило, transparent trait — это trait-ы, влияющие на реализацию наследуемых классов, и trait-ы, которые сами по себе обычно не используются как типы. Два примера из стандартной библиотеки коллекций:

Как правило, любой trait, расширяемый рекурсивно, является хорошим кандидатом на объявление transparent.

Правила вывода типов говорят, что transparent trait удаляются из пересечений, где это возможно.


Ссылки: