В чем разница между самоподтипами и подклассами черт?

Тип личности для черты A:

trait B
trait A { this: B => }

говорит, что " A не может быть смешан в конкретный класс, который также не распространяется B "

С другой стороны, следующее:

trait B
trait A extends B

говорит, что "любой (конкретный или абстрактный) класс смешивается в A также будет смешивать в B ".

Разве эти два утверждения не означают одно и то же? Кажется, что self-type служит только для того, чтобы создать возможность простой ошибки во время компиляции.

Что мне не хватает?

11 ответов

Решение

Он преимущественно используется для инъекций зависимости, например, в Cake Pattern. В Scala есть отличная статья, рассказывающая о различных формах внедрения зависимостей, включая Cake Pattern. Если вы используете Google "Cake Pattern and Scala", вы получите много ссылок, включая презентации и видео. А пока вот ссылка на другой вопрос.

Теперь, что касается разницы между типом личности и расширением черты, это просто. Если вы говорите B extends A, затем B это A, Когда вы используете самотипы, B требует A, Существуют два конкретных требования, которые создаются с помощью собственных типов:

  1. Если B продлевается, то вы должны добавить A,
  2. Когда конкретный класс наконец расширяет / смешивает эти черты, некоторый класс / черта должен реализовать A,

Рассмотрим следующие примеры:

scala> trait User { def name: String }
defined trait User

scala> trait Tweeter {
     |   user: User =>
     |   def tweet(msg: String) = println(s"$name: $msg")
     | }
defined trait Tweeter

scala> trait Wrong extends Tweeter {
     |   def noCanDo = name
     | }
<console>:9: error: illegal inheritance;
 self-type Wrong does not conform to Tweeter's selftype Tweeter with User
       trait Wrong extends Tweeter {
                           ^
<console>:10: error: not found: value name
         def noCanDo = name
                       ^

Если Tweeter был подклассом User не было бы ошибки. В приведенном выше коде мы потребовали User всякий раз, когда Tweeter используется, однако User не был предоставлен Wrong Итак, мы получили ошибку. Теперь, когда код выше по-прежнему находится в области видимости, рассмотрим:

scala> trait DummyUser extends User {
     |   override def name: String = "foo"
     | }
defined trait DummyUser

scala> trait Right extends Tweeter with User {
     |   val canDo = name
     | }
defined trait Right 

scala> trait RightAgain extends Tweeter with DummyUser {
     |   val canDo = name
     | }
defined trait RightAgain

С Right, требование смешать в User доволен. Однако второе требование, упомянутое выше, не выполняется: бремя реализации User все еще остается для классов / черт, которые расширяются Right,

С RightAgain оба требования выполнены. User и реализация User предоставлены.

Для более практического использования, пожалуйста, смотрите ссылки в начале этого ответа! Но, надеюсь, теперь вы получите это.

Самостоятельные типы позволяют вам определять циклические зависимости. Например, вы можете достичь этого:

trait A { self: B => }
trait B { self: A => }

Наследование с использованием extends не позволяет этого. Пытаться:

trait A extends B
trait B extends A
error:  illegal cyclic reference involving trait A

В книге Одерского посмотрите раздел 33.5 (Глава "Создание пользовательского интерфейса электронной таблицы"), где упоминается:

В примере электронной таблицы класс Model наследуется от Evaluator и, таким образом, получает доступ к его методу оценки. Чтобы пойти другим путем, класс Evaluator определяет свой собственный тип как Model:

package org.stairwaybook.scells
trait Evaluator { this: Model => ...

Надеюсь это поможет.

Еще одно отличие состоит в том, что self-типы могут указывать не классовые типы. Например

trait Foo{
   this: { def close:Unit} => 
   ...
}

Тип self здесь является структурным типом. Эффект состоит в том, чтобы сказать, что все, что смешивается в Foo, должно реализовывать безошибочный метод "close", возвращающий единицу. Это позволяет использовать безопасные миксины для утки.

Еще одна вещь, которая не была упомянута: поскольку самотипы не являются частью иерархии требуемого класса, они могут быть исключены из сопоставления с образцом, особенно когда вы исчерпывающе сопоставляете запечатанную иерархию. Это удобно, когда вы хотите моделировать ортогональные поведения, такие как:

sealed trait Person
trait Student extends Person
trait Teacher extends Person
trait Adult { this : Person => } // orthogonal to its condition

val p : Person = new Student {}
p match {
  case s : Student => println("a student")
  case t : Teacher => println("a teacher")
} // that's it we're exhaustive

TL;DR резюме других ответов:

  • Типы, которые вы расширяете, подвержены унаследованным типам, но сами типы не

    например: class Cow { this: FourStomachs } позволяет использовать методы, доступные только для жвачных животных, такие как digestGrass, Черты, которые расширяют корову, однако, не будут иметь таких привилегий. С другой стороны, class Cow extends FourStomachs выставлю digestGrass любому, кто extends Cow,

  • Self-типы допускают циклические зависимости, расширение других типов не допускает

Раздел 2.3 "Аннотации к самотипу" оригинальной статьи Scala, посвященной абстракциям Scalable Component, выполненной Мартином Одерским, на самом деле очень хорошо объясняет цель самотипирования помимо миксиновой композиции: предоставляет альтернативный способ связывания класса с абстрактным типом.

Пример, приведенный в статье, был похож на следующий, и у него, похоже, нет элегантного корреспондента подкласса:

abstract class Graph {
  type Node <: BaseNode;
  class BaseNode {
    self: Node =>
    def connectWith(n: Node): Edge =
      new Edge(self, n);
  }
  class Edge(from: Node, to: Node) {
    def source() = from;
    def target() = to;
  }
}

class LabeledGraph extends Graph {
  class Node(label: String) extends BaseNode {
    def getLabel: String = label;
    def self: Node = this;
  }
}

Начнем с циклической зависимости.

trait A {
  selfA: B =>
  def fa: Int }

trait B {
  selfB: A =>
  def fb: String }

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

trait A1 extends A {
  selfA1: B =>
  override def fb = "B's String" }
trait B1 extends B {
  selfB1: A =>
  override def fa = "A's String" }
val myObj = new A1 with B1

Хотя, если вы переопределите член собственного типа, вы потеряете доступ к исходному члену, к которому все еще можно получить доступ через super с использованием наследования. Итак, что действительно получено за счет наследования:

trait AB {
  def fa: String
  def fb: String }
trait A1 extends AB
{ override def fa = "A's String" }        
trait B1 extends AB
{ override def fb = "B's String" }    
val myObj = new A1 with B1

Теперь я не могу утверждать, что понимаю все тонкости шаблона торта, но мне кажется, что основной метод обеспечения модульности - это составление, а не наследование или самопечатание.

Версия наследования короче, но главная причина, по которой я предпочитаю наследование по сравнению с типами self, состоит в том, что мне гораздо сложнее получить правильный порядок инициализации с типами self. Однако есть некоторые вещи, которые вы можете делать с типами себя, которые вы не можете делать с наследованием. Self-типы могут использовать тип, в то время как наследование требует черты или класса, как в:

trait Outer
{ type T1 }     
trait S1
{ selfS1: Outer#T1 => } //Not possible with inheritance.

Вы даже можете сделать:

trait TypeBuster
{ this: Int with String => }

Хотя ты никогда не сможешь создать его экземпляр. Я не вижу какой-либо абсолютной причины для того, чтобы не иметь возможности наследовать от типа, но я, конечно, чувствую, что было бы полезно иметь классы и признаки конструктора путей, поскольку у нас есть признаки / классы конструктора типов. Как к сожалению

trait InnerA extends Outer#Inner //Doesn't compile

У нас есть это:

trait Outer
{ trait Inner }
trait OuterA extends Outer
{ trait InnerA extends Inner }
trait OuterB extends Outer
{ trait InnerB extends Inner }
trait OuterFinal extends OuterA with OuterB
{ val myV = new InnerA with InnerB }

Или это:

  trait Outer
  { trait Inner }     
  trait InnerA
  {this: Outer#Inner =>}
  trait InnerB
  {this: Outer#Inner =>}
  trait OuterFinal extends Outer
  { val myVal = new InnerA with InnerB with Inner }

Еще один момент, на который следует обратить особое внимание, заключается в том, что черты могут расширять классы. Спасибо Дэвиду Маклверу за указание на это. Вот пример из моего собственного кода:

class ScnBase extends Frame
abstract class ScnVista[GT <: GeomBase[_ <: TypesD]](geomRI: GT) extends ScnBase with DescripHolder[GT] )
{ val geomR = geomRI }    
trait EditScn[GT <: GeomBase[_ <: ScenTypes]] extends ScnVista[GT]
trait ScnVistaCyl[GT <: GeomBase[_ <: ScenTypes]] extends ScnVista[GT]

ScnBase наследуется от класса Swing Frame, поэтому его можно использовать как собственный тип, а затем смешивать в конце (при создании экземпляра). Тем не мение, val geomR должен быть инициализирован перед использованием наследования черт. Таким образом, нам нужен класс для обеспечения предварительной инициализации geomR, Класс ScnVista затем может наследоваться от нескольких ортогональных признаков, которые сами могут наследоваться от. Использование параметров нескольких типов (обобщений) предлагает альтернативную форму модульности.

trait A { def x = 1 }
trait B extends A { override def x = super.x * 5 }
trait C1 extends B { override def x = 2 }
trait C2 extends A { this: B => override def x = 2}

// 1.
println((new C1 with B).x) // 2
println((new C2 with B).x) // 10

// 2.
trait X {
  type SomeA <: A
  trait Inner1 { this: SomeA => } // compiles ok
  trait Inner2 extends SomeA {} // doesn't compile
}

Тип self позволяет указать, какие типы могут смешиваться в признаке. Например, если у вас есть черта с собственным типом Closeableто эта черта знает, что единственные вещи, которые могут смешиваться, должны реализовывать Closeable интерфейс.

Обновление: принципиальное отличие состоит в том, что самотипы могут зависеть от нескольких классов (я допускаю, что это немного угловой случай). Например, вы можете иметь

class Person {
  //...
  def name: String = "...";
}

class Expense {
  def cost: Int = 123;
}

trait Employee {
  this: Person with Expense =>
  // ...

  def roomNo: Int;

  def officeLabel: String = name + "/" + roomNo;
}

Это позволяет добавить Employee Mixin просто ко всему, что является подклассом Person а также Expense, Конечно, это имеет смысл, только если Expense продолжается Person или наоборот. Дело в том, что с помощью самопознания Employee может быть независимым от иерархии классов, от которых зависит. Это не заботится о том, что расширяет что - Если вы переключаете иерархию Expense против PersonВам не нужно изменять Employee,

В первом случае под-признак или подкласс B может быть смешан с тем, что использует A. Таким образом, B может быть абстрактным признаком.

Другие вопросы по тегам