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

Самотип для признака A:

trait B
trait A { this: B => }

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

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

trait B
trait A extends B

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

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

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


person Dave    schedule 02.01.2010    source источник
comment
Меня действительно интересуют различия между типами себя и подклассами по признакам. Я действительно знаю некоторые из общих применений самотипов; Я просто не могу найти причину, по которой они не стали бы более четко делать то же самое с подтипами.   -  person Dave    schedule 03.01.2010
comment
Можно использовать параметры типа в самотипах: trait A[Self] {this: Self => } разрешено, trait A[Self] extends Self - нет.   -  person Blaisorblade    schedule 20.01.2013
comment
Собственный тип также может быть классом, но признак не может наследовать от класса.   -  person cvogt    schedule 22.06.2013
comment
@cvogt: признак может наследовать от класса (по крайней мере, с 2.10): pastebin.com/zShvr8LX   -  person Erik Kaplun    schedule 14.03.2014
comment
@Blaisorblade: разве это не то, что можно решить с помощью небольшого редизайна языка, а не фундаментальное ограничение? (по крайней мере, с точки зрения вопроса)   -  person Erik Kaplun    schedule 14.03.2014
comment
@ErikAllik: Я узнал об этом ограничении из статьи, описывающей шаблон торта, масштабируемые абстракции компонентов, поэтому я сомневаюсь, что это случайность. Я подозреваю, что причина заключается просто в ограничениях JVM, а не в глубоких мотивах, но это не обязательно означает, что исправление возможно.   -  person Blaisorblade    schedule 15.03.2014
comment
Я обнаружил, что этот самотип очень полезен для наложения классов, реализующих признак, который они расширяют (запечатанный) trait-Enum. См. Это: stackoverflow.com/q/36066238/1206998   -  person Juh_    schedule 17.03.2016


Ответы (10)


Он преимущественно используется для внедрения зависимостей, например, в шаблоне торта. Существует отличная статья, охватывающая множество различных форм внедрения зависимостей в Scala. , включая выкройку для торта. Если вы погуглите "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.

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

person Daniel C. Sobral    schedule 02.01.2010
comment
Спасибо. Шаблон «Торт» - это 90% того, что я имею в виду, почему я говорю о шумихе вокруг самотипов ... именно здесь я впервые увидел эту тему. Пример Йонаса Бонера великолепен, потому что он подчеркивает суть моего вопроса. Если вы измените self-типы в его примере с нагревателем на вычитания, то в чем будет разница (кроме ошибки, которую вы получаете при определении ComponentRegistry, если вы не смешиваете правильные вещи? - person Dave; 03.01.2010
comment
@ Дэйв: Ты имеешь в виду, как trait WarmerComponentImpl extends SensorDeviceComponent with OnOffDeviceComponent? Это заставит WarmerComponentImpl иметь эти интерфейсы. Они будут доступны для всего, что расширяет WarmerComponentImpl, что явно неверно, поскольку это не ни SensorDeviceComponent, ни OnOffDeviceComponent. Как самостоятельный тип, эти зависимости доступны исключительно для WarmerComponentImpl. List можно использовать как Array, и наоборот. Но это не одно и то же. - person Daniel C. Sobral; 03.01.2010
comment
Спасибо, Даниэль. Вероятно, это главное отличие, которое я искал. Практическая проблема заключается в том, что использование подклассов приведет к утечке функций в ваш интерфейс, которые вы не собираетесь использовать. Это результат нарушения более теоретических правил для черт. Самотипы выражают использование - отношения между частями. - person Dave; 03.01.2010
comment
@ Дэйв: Да. Возможно, интересно отметить, что черты и самотипы, возможно, являются наиболее важными при использовании объекта в качестве модулей. По крайней мере, так я это вижу. Не обязательно object, который является синглтоном. - person Daniel C. Sobral; 04.01.2010
comment
Вторая черта должна читаться так: User = ›вместо user: User =› - person Rodney Gitzel; 19.01.2011
comment
@Rodney Нет, не должно. Фактически, использование this с самотипами - это то, на что я смотрю свысока, поскольку оно без всякой уважительной причины затмевает исходный this. - person Daniel C. Sobral; 19.01.2011
comment
Хм. Ах, моя беда, когда я вставил пользователя: у меня ошибка вставки, а не ошибка компиляции. Но возникает вопрос, зачем вообще называть самотип? Чтобы устранить двусмысленность, я предполагаю: user.name можно использовать вместо просто name. Так что однозначно использовать это - плохая идея ... как бы вы могли получить доступ к настоящему? - person Rodney Gitzel; 19.01.2011
comment
URL статьи, опубликованной Mushtaq, немного изменился: jonasboner.com/2008/10/06/ - person Andrew E; 01.07.2012
comment
Отличный ответ, кстати, как вы можете выразить более одной зависимости? Я пробовал с (dep1: Dep1, dep2: Dep2) = ›{xxx}, и он компилируется, но не проверяет зависимости ... - person opensas; 25.11.2012
comment
@opensas Попробуйте self: Dep1 with Dep2 =>. - person Daniel C. Sobral; 03.12.2012
comment
Итак, @ DanielC.Sobral, добавление ... with User к trait Wrong extends Tweeter устранит эту проблему, поскольку использование self-trait означает, что он должен иметь User? - person Kevin Meredith; 08.01.2015
comment
Спасибо за такой ответ! Может ли кто-нибудь указать на разницу между написанием user: User =>, self: User => и this: User =>? - person Ivaylo Toskov; 20.02.2017
comment
@IvayloToskov они почти идентичны. Все они требуют, чтобы тип this был User, но они, возможно, также вводят идентификатор user или self в качестве псевдонима для this. Таким образом, с trait Tweeter { user: User => вы можете написать user.name в дополнение к this.name внутри Tweeter. - person BrunoMedeiros; 26.07.2017
comment
@ DanielC.Sobral Я не думаю, что ваш ответ правильный: вы сказали, что когда вы выполняете инъекцию зависимостей, вы хотите, чтобы B требовал A, а не был A. - ну, это может быть верно для DI в целом, но не для синтаксис самотипа, обсуждаемый здесь. А именно, в вашем примере: trait Tweeter { user: User => // ... , объявление собственного типа действительно требует конкретных типов, реализующих Tweeter, чтобы также быть User (аналогично наличию trait Tweeter extends User вместо этого). Вот почему trait Wrong extends Tweeter - ошибка, а trait Wrong extends Tweeter with User { работает. - person BrunoMedeiros; 26.07.2017
comment
URL статьи снова изменился: jonasboner.com/real-world-scala-dependency -injection-di - person ari gold; 25.04.2018
comment
держите свои ответы короткими или добавляйте tl; ответ dr @ jazmit ниже может дать вам подсказку - person MaxNevermind; 21.07.2018

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

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 => ...

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

person Mushtaq Ahmed    schedule 02.01.2010
comment
Я не рассматривал этот сценарий. Это первый пример того, что я видел, что не то же самое, что самотип, как с подклассом. Тем не менее, это кажется своего рода крайним случаем и, что более важно, кажется плохой идеей (я обычно стараюсь изо всех сил НЕ определять циклические зависимости!). Считаете ли вы это самым важным отличием? - person Dave; 03.01.2010
comment
Я так думаю. Я не вижу другой причины, по которой я бы предпочел самотипы вместо предложения extends. Самотипы многословны, они не наследуются (поэтому вы должны добавить самотипы ко всем подтипам в качестве ритуала), и вы можете видеть только член, но не можете их переопределить. Я хорошо знаком с паттерном Cake и множеством постов, в которых упоминаются самотипы для DI. Но почему-то я не уверен. Я уже давно создал здесь образец приложения (bitbucket.org/mushtaq/scala-di ). Посмотрите конкретно папку / src / configs. Я добился DI для замены сложных конфигураций Spring без самотипов. - person Mushtaq Ahmed; 03.01.2010
comment
Муштак, мы согласны. Я думаю, что заявление Дэниела о недопущении раскрытия непреднамеренной функциональности является важным, но, как вы выразились, существует зеркальное отражение этой «функции» ... что вы не можете переопределить функциональность или использовать ее в будущих подклассах. Это довольно ясно говорит мне, когда дизайн будет требовать одно над другим. Я буду избегать самотипирования до тех пор, пока не найду настоящую потребность - то есть, если я начну использовать объекты как модули, как указывает Даниэль. Я автоматически подключаю зависимости с неявными параметрами и простым объектом начальной загрузки. Мне нравится простота. - person Dave; 04.01.2010
comment
@ DanielC.Sobral может быть благодаря вашему комментарию, но на данный момент у него больше голосов, чем у вашего ансера. Проголосовать за обоих :) - person rintcius; 14.09.2012
comment
Почему бы просто не создать одну черту AB? Поскольку черты A и B всегда должны быть объединены в любом конечном классе, зачем их разделять в первую очередь? - person Rich Oliver; 05.11.2019
comment
@RichOliver Я думаю, ответ в том, что вы просто не контролируете каждую черту во вселенной. - person Kevin Dreßler; 16.07.2020

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

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

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

person Dave Griffith    schedule 21.06.2010
comment
На самом деле вы можете использовать наследование и со структурными типами: абстрактный класс A extends {def close: Unit} - person Adrian; 22.03.2011
comment
Я думаю, что структурная типизация использует отражение, поэтому используйте только тогда, когда нет другого выбора ... - person Eran Medan; 08.07.2013
comment
@ Адриан, я считаю, что ваш комментарий неверен. Абстрактный класс A extends {def close: Unit} - это просто абстрактный класс с суперклассом Object. это просто разрешающий синтаксис Scala для бессмысленных выражений. Вы можете `class X extends {def f = 1}; new X (). f` например - person Alexey; 14.06.2016
comment
@Alexey Я не понимаю, почему ваш (или мой) пример бессмысленен. - person Adrian; 09.07.2016
comment
@ Адриан, abstract class A extends {def close:Unit} эквивалентно abstract class A {def close:Unit}. Так что это не касается структурных типов. - person Alexey; 09.07.2016

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

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
person Bruno Bieth    schedule 03.06.2015

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

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

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;
  }
}
person lcn    schedule 14.09.2014
comment
Для тех, кто задается вопросом, почему подклассы не решают эту проблему, в Разделе 2.3 также говорится следующее: «Каждый из операндов композиции миксина C_0 с ... с C_n должен ссылаться на класс. Механизм композиции миксинов не позволяет C_i ссылаться на абстрактный тип. Это ограничение позволяет статически проверять двусмысленность и преодолевать конфликты в точке, где создается класс ». - person Luke Maurer; 28.07.2017

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

  • Расширяемые типы доступны для унаследованных типов, но самотипы - нет.

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

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

person jazmit    schedule 02.06.2015

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

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

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

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

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

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

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 может быть унаследован от нескольких ортогональных признаков, от которых могут быть унаследованы сами. Использование нескольких параметров типа (обобщенных) предлагает альтернативную форму модульности.

person Rich Oliver    schedule 06.10.2012

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

person kikibobo    schedule 02.01.2010
comment
@Blaisorblade: Интересно, возможно, вы неправильно прочитали ответ Кикибобо - собственный тип черты действительно позволяет вам ограничивать типы, которые могут смешивать его, и это часть его полезности. Например, если мы определяем trait A { self:B => ... }, то объявление X with A допустимо только в том случае, если X расширяет B. Да, вы можете сказать X with A with Q, где Q не расширяет B, но я считаю, что точка зрения kikibobo заключалась в том, что X так ограничен. Или я что-то упустил? - person AmigoNico; 20.01.2013
comment
Спасибо, ты прав. Мой голос был заблокирован, но, к счастью, я смог отредактировать ответ, а затем изменить свой голос. - person Blaisorblade; 20.01.2013

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

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

person Petr    schedule 07.10.2012
comment
Employee не обязательно должен быть классом, чтобы происходить от Person. Черты могут расширять классы. Если черта Employee расширит Person вместо использования self, пример все равно будет работать. Я нахожу ваш пример интересным, но, похоже, он не иллюстрирует вариант использования самотипов. - person Morgan Creighton; 16.10.2012
comment
@MorganCreighton Достаточно честно, я не знал, что черты могут расширять классы. Я подумаю, если найду лучший пример. - person Petr; 16.10.2012
comment
Да, это удивительная языковая особенность. Если черта Employee расширяет класс Person, то любой класс, который в конечном итоге остается в Employee, также должен расширять Person. Но это ограничение все еще присутствует, если Employee использовал собственный тип вместо расширения Person. Ура, Петр! - person Morgan Creighton; 18.10.2012
comment
Я не понимаю, почему это имеет смысл только в том случае, если Expense расширяет Person или наоборот. - person Robin Green; 31.05.2015

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

person IttayD    schedule 02.01.2010
comment
Нет, B может быть (и действительно является) абстрактной чертой в обоих случаях. Так что с этой точки зрения разницы нет. - person Robin Green; 31.05.2015