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

Автотип для признака A:

trait B
trait A { this: B => }

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

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

trait B
trait A extends B

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

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

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

Ответы

Ответ 1

Он преимущественно используется для инъекций зависимости, например, в 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 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 предоставляются.

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

Ответ 2

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

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

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

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

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

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

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

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

Ответ 3

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

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

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

Ответ 4

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

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

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;
  }
}

Ответ 5

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

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 it we're exhaustive

Ответ 6

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

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

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

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

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

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

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

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

Версия наследования короче, но основная причина, по которой я предпочитаю наследование по типам 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 Класс кадра, поэтому его можно использовать как тип self, а затем смешивать в конце (при создании экземпляра). Тем не менее, val geomR необходимо инициализировать, прежде чем использовать его, наследуя признаки. Поэтому нам нужен класс для принудительной инициализации geomR. Класс ScnVista может затем наследоваться из-за множества ортогональных признаков, которые сами могут быть унаследованы. Использование нескольких параметров типа (generics) предлагает альтернативную форму модульности.

Ответ 7

TL; DR сводка других ответов:

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

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

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

Ответ 8

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
}

Ответ 9

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

Ответ 10

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

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 только к любому подклассу Person и Expense. Конечно, это имеет смысл только в том случае, если Expense продолжается Person или наоборот. Дело в том, что использование self-types Employee может быть независимым от иерархии классов, от которых он зависит. Он не заботится о том, что расширяет то, что. Если вы переключите иерархию Expense vs Person, вам не нужно изменять Employee.

Ответ 11

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