Apa perbedaan antara tipe-diri dan subkelas sifat?

387

Tipe diri untuk suatu sifat A:

trait B
trait A { this: B => }

mengatakan bahwa " Atidak dapat dicampur ke dalam kelas konkret yang tidak juga diperluas B" .

Di sisi lain, berikut ini:

trait B
trait A extends B

mengatakan bahwa "setiap pencampuran kelas (konkret atau abstrak) Ajuga akan bercampur dalam B" .

Bukankah kedua pernyataan ini memiliki arti yang sama? Tipe diri tampaknya hanya berfungsi untuk menciptakan kemungkinan kesalahan waktu kompilasi sederhana.

Apa yang saya lewatkan?

Dave
sumber
Saya sebenarnya tertarik di sini dalam perbedaan antara tipe diri dan subclass dalam sifat. Saya tahu beberapa kegunaan umum untuk tipe diri; Saya tidak bisa menemukan alasan mengapa mereka tidak akan lebih jelas dilakukan dengan cara yang sama dengan subtyping.
Dave
32
Seseorang dapat menggunakan parameter tipe dalam tipe diri: trait A[Self] {this: Self => }legal, trait A[Self] extends Selfbukan.
Blaisorblade
3
Tipe diri juga bisa menjadi kelas, tetapi suatu sifat tidak bisa mewarisi dari kelas.
cvogt
10
@cvogt: suatu sifat dapat diwarisi dari suatu kelas (setidaknya pada 2.10): pastebin.com/zShvr8LX
Erik Kaplun
1
@ Blaisorblade: bukankah itu sesuatu yang bisa diselesaikan dengan desain ulang bahasa kecil, dan bukan batasan mendasar? (setidaknya dari sudut pandang pertanyaan)
Erik Kaplun

Jawaban:

273

Ini terutama digunakan untuk Injeksi Ketergantungan , seperti dalam Pola Kue. Ada artikel bagus yang membahas berbagai bentuk injeksi ketergantungan di Scala, termasuk Pola Kue. Jika Anda Google "Pola Kue dan Scala", Anda akan mendapatkan banyak tautan, termasuk presentasi dan video. Untuk saat ini, di sini ada tautan ke pertanyaan lain .

Sekarang, seperti apa perbedaan antara tipe diri dan memperluas sifat, itu sederhana. Jika Anda mengatakan B extends A, maka B adalah sebuah A. Ketika Anda menggunakan tipe-diri, B dibutuhkan sebuah A. Ada dua persyaratan khusus yang dibuat dengan tipe mandiri:

  1. Jika Bdiperpanjang, maka Anda harus mencampur A.
  2. Ketika kelas konkret akhirnya meluas / mencampur-dalam sifat-sifat ini, beberapa kelas / sifat harus diimplementasikan A.

Perhatikan contoh-contoh berikut:

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
                       ^

Jika Tweeteritu adalah subkelas dari User, tidak akan ada kesalahan. Pada kode di atas, kita diperlukan sebuah Usersetiap kali Tweeterdigunakan, namun Usertidak disediakan untuk Wrong, jadi kita punya kesalahan. Sekarang, dengan kode di atas masih dalam cakupan, pertimbangkan:

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

Dengan Right, persyaratan untuk mencampuradukkan Useradalah terpenuhi. Namun, persyaratan kedua yang disebutkan di atas tidak puas: beban implementasi Usermasih ada untuk kelas / sifat yang meluas Right.

Dengan RightAgainkedua persyaratan terpenuhi. A Userdan implementasi Userdisediakan.

Untuk kasus penggunaan yang lebih praktis, silakan lihat tautan di awal jawaban ini! Tapi, semoga sekarang Anda mengerti.

Daniel C. Sobral
sumber
3
Terima kasih. Pola Kue adalah 90% dari apa yang saya maksudkan mengapa saya berbicara tentang hype di sekitar tipe diri ... itu adalah tempat saya pertama kali melihat topik. Teladan Jonas Boner luar biasa karena menggarisbawahi inti pertanyaan saya. Jika Anda mengubah tipe-diri dalam contoh pemanasnya menjadi subtraits, lalu apa bedanya (selain kesalahan yang Anda dapatkan saat mendefinisikan ComponentRegistry jika Anda tidak mencampurkan hal-hal yang benar?
Dave
29
@ Dave: Maksud Anda suka trait WarmerComponentImpl extends SensorDeviceComponent with OnOffDeviceComponent? Itu akan menyebabkan WarmerComponentImplmemiliki antarmuka tersebut. Mereka akan tersedia untuk apa pun yang diperpanjang WarmerComponentImpl, yang jelas salah, karena tidak seorang SensorDeviceComponent, ataupun OnOffDeviceComponent. Sebagai tipe mandiri, dependensi ini tersedia secara eksklusif untuk WarmerComponentImpl. A Listdapat digunakan sebagai Array, dan sebaliknya. Tapi mereka tidak sama.
Daniel C. Sobral
10
Terima kasih Daniel. Ini mungkin perbedaan utama yang saya cari. Masalah praktisnya adalah bahwa menggunakan subclassing akan membocorkan fungsionalitas ke antarmuka Anda yang tidak Anda inginkan. Ini adalah hasil dari pelanggaran terhadap aturan "is-part-of-a" yang lebih teoretis. Tipe-diri mengekspresikan hubungan "menggunakan-a" antara bagian-bagian.
Dave
11
@Rodney Tidak, seharusnya tidak. Bahkan, menggunakan thisdengan tipe-tipe diri adalah sesuatu yang saya pandang rendah, karena bayangan itu tanpa alasan asli this.
Daniel C. Sobral
9
@opensas Coba self: Dep1 with Dep2 =>.
Daniel C. Sobral
156

Jenis mandiri memungkinkan Anda untuk menentukan dependensi siklus. Misalnya, Anda dapat mencapai ini:

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

Penggunaan warisan extendstidak memungkinkan hal itu. Mencoba:

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

Dalam buku Odersky, lihat bagian 33.5 (Membuat bab UI lembar bentang) di mana ia menyebutkan:

Dalam contoh lembar bentang, Model kelas mewarisi dari Evaluator dan karenanya memperoleh akses ke metode evaluasinya. Dengan kata lain, class Evaluator mendefinisikan tipe dirinya sebagai Model, seperti ini:

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

Semoga ini membantu.

Mushtaq Ahmed
sumber
3
Saya belum mempertimbangkan skenario ini. Ini adalah contoh pertama dari sesuatu yang pernah saya lihat yang tidak sama dengan tipe-diri seperti halnya dengan subkelas. Namun, sepertinya agak casey dan, yang lebih penting, sepertinya ide yang buruk (saya biasanya pergi jauh dari jalan saya TIDAK untuk mendefinisikan dependensi siklik!). Apakah Anda menganggap ini sebagai perbedaan yang paling penting?
Dave
4
Aku pikir begitu. Saya tidak melihat alasan lain mengapa saya lebih suka tipe diri untuk memperluas klausa. Tipe-diri adalah kata kerja, mereka tidak diwariskan (jadi Anda harus menambahkan tipe-diri ke semua subtipe sebagai ritual) dan Anda hanya dapat melihat anggota tetapi tidak dapat menimpanya. Saya sangat menyadari pola Cake dan banyak posting yang menyebutkan tipe diri untuk DI. Tetapi entah bagaimana saya tidak yakin. Saya telah membuat aplikasi sampel di sini sejak lama ( bitbucket.org/mushtaq/scala-di ). Lihat secara khusus di folder / src / configs. Saya mencapai DI untuk mengganti konfigurasi Spring yang kompleks tanpa tipe mandiri.
Mushtaq Ahmed
Mushtaq, kami sepakat. Saya pikir pernyataan Daniel tentang tidak mengekspos fungsionalitas yang tidak disengaja adalah yang penting tetapi, seperti yang Anda katakan, ada tampilan cermin dari 'fitur' ini ... bahwa Anda tidak dapat mengesampingkan fungsionalitas atau menggunakannya dalam subkelas masa depan. Ini cukup jelas memberitahu saya kapan desain akan memanggil satu dari yang lain. Saya akan menghindari tipe-diri sampai saya menemukan kebutuhan asli - yaitu jika saya mulai menggunakan objek sebagai modul seperti yang ditunjukkan Daniel. Saya autowiring dependensi dengan parameter implisit dan objek bootstrapper langsung. Saya suka kesederhanaannya.
Dave
@ DanielC.Sobral mungkin berkat komentar Anda, tetapi saat ini memiliki lebih banyak upvotes daripada anser Anda. Upvoting keduanya :)
rintcius
Mengapa tidak hanya membuat satu sifat AB? Karena sifat A dan B harus selalu digabungkan dalam kelas akhir mana pun, mengapa harus memisahkannya?
Rich Oliver
56

Satu perbedaan tambahan adalah bahwa tipe diri dapat menentukan tipe non-kelas. Contohnya

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

Tipe mandiri di sini adalah tipe struktural. Efeknya adalah untuk mengatakan bahwa apa pun yang bercampur di Foo harus menerapkan unit pengembalian metode "tutup" no-arg. Ini memungkinkan mixin aman untuk mengetik bebek.

Dave Griffith
sumber
41
Sebenarnya Anda dapat menggunakan warisan dengan tipe struktural juga: kelas abstrak A extends {def close: Unit}
Adrian
12
Saya pikir pengetikan struktural menggunakan refleksi, jadi gunakan hanya ketika tidak ada pilihan lain ...
Eran Medan
@Adrian, saya yakin komentar Anda salah. `abstract class A extends {def close: Unit}` hanyalah kelas abstrak dengan superclass Object. itu hanya sintaks permisif Scala untuk ekspresi tidak masuk akal. Anda dapat `kelas X extends {def f = 1}; new X (). f` misalnya
Alexey
1
@ Alexey Saya tidak melihat mengapa contoh Anda (atau milik saya) tidak masuk akal.
Adrian
1
@Adrian, abstract class A extends {def close:Unit}setara dengan abstract class A {def close:Unit}. Jadi tidak melibatkan tipe struktural.
Alexey
13

Bagian 2.3 "Anotasi Selftipe" dari makalah Scalaable Scalable Component Abstraksi karya Martin Odersky sebenarnya menjelaskan tujuan selftype di luar komposisi mixin dengan sangat baik: memberikan cara alternatif untuk mengasosiasikan kelas dengan jenis abstrak.

Contoh yang diberikan di koran seperti berikut ini, dan sepertinya tidak memiliki koresponden subclass yang elegan:

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;
  }
}
lcn
sumber
Bagi mereka yang bertanya-tanya mengapa subclassing tidak akan menyelesaikan ini, Bagian 2.3 juga mengatakan ini: “Setiap operan dari komposisi mixin C_0 dengan ... dengan C_n, harus merujuk ke kelas. Mekanisme komposisi mixin tidak memungkinkan C_i untuk merujuk pada tipe abstrak. Pembatasan ini memungkinkan untuk memeriksa ambiguitas dan menimpa konflik secara statis pada titik di mana kelas dikomposisikan. "
Luke Maurer
12

Hal lain yang belum disebutkan: karena tipe-diri bukan bagian dari hierarki kelas yang diperlukan, mereka dapat dikecualikan dari pencocokan pola, terutama ketika Anda secara menyeluruh mencocokkan dengan hierarki yang disegel. Ini nyaman ketika Anda ingin memodelkan perilaku ortogonal seperti:

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
Bruno Bieth
sumber
10

TL; DR ringkasan jawaban lain:

  • Tipe yang Anda rentangkan terkena tipe yang diwarisi, tetapi tipe diri tidak

    misalnya: class Cow { this: FourStomachs }memungkinkan Anda menggunakan metode yang hanya tersedia untuk ruminansia, seperti digestGrass. Namun sifat-sifat yang memperpanjang Sapi tidak akan memiliki hak istimewa seperti itu. Di sisi lain, class Cow extends FourStomachsakan mengekspos digestGrasskepada siapa saja yang extends Cow .

  • tipe diri memungkinkan dependensi siklus, memperluas tipe lain tidak

jazmit
sumber
9

Mari kita mulai dengan ketergantungan siklus.

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

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

Namun, modularitas dari solusi ini tidak sebesar yang pertama kali muncul, karena Anda dapat mengesampingkan tipe diri seperti itu:

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

Meskipun, jika Anda menimpa anggota tipe diri, Anda kehilangan akses ke anggota asli, yang masih dapat diakses melalui super menggunakan pewarisan. Jadi yang benar-benar didapat dari menggunakan warisan adalah:

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

Sekarang saya tidak bisa mengklaim memahami semua seluk-beluk pola kue, tetapi menurut saya metode utama menegakkan modularitas adalah melalui komposisi daripada tipe pewarisan atau mandiri.

Versi warisan lebih pendek, tetapi alasan utama saya lebih memilih warisan daripada tipe diri adalah bahwa saya merasa jauh lebih sulit untuk mendapatkan urutan inisialisasi yang benar dengan tipe diri. Namun, ada beberapa hal yang dapat Anda lakukan dengan tipe diri yang tidak dapat Anda lakukan dengan pewarisan. Tipe mandiri dapat menggunakan tipe sementara pewarisan memerlukan sifat atau kelas seperti pada:

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

Anda bahkan dapat melakukannya:

trait TypeBuster
{ this: Int with String => }

Meskipun Anda tidak akan pernah bisa instantiate. Saya tidak melihat alasan mutlak untuk tidak dapat mewarisi dari suatu tipe, tetapi saya tentu merasa akan berguna untuk memiliki kelas dan sifat path konstruktor karena kami memiliki tipe / kelas konstruktor tipe. Sayangnya

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

Kami punya ini:

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 }

Atau ini:

  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 }

Satu hal yang harus lebih ditekankan adalah bahwa sifat dapat memperluas kelas. Terima kasih kepada David Maclver karena menunjukkan ini. Berikut ini contoh dari kode saya sendiri:

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]

ScnBasemewarisi dari kelas Swing Frame, sehingga dapat digunakan sebagai tipe mandiri dan kemudian dicampur di akhir (di instantiation). Namun, val geomRperlu diinisialisasi sebelum digunakan oleh pewarisan sifat. Jadi kita perlu kelas untuk menegakkan inisialisasi sebelumnya geomR. Kelas ScnVistakemudian dapat diwarisi dari beberapa sifat ortogonal yang dapat diwarisi sendiri. Menggunakan beberapa tipe parameter (generik) menawarkan bentuk modularitas alternatif.

Oliver yang kaya
sumber
7
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
}
Oleg Galako
sumber
4

Tipe mandiri memungkinkan Anda menentukan tipe apa yang diizinkan untuk menggabungkan suatu sifat. Misalnya, jika Anda memiliki sifat dengan tipe diri Closeable, maka sifat itu tahu bahwa satu-satunya hal yang diizinkan untuk menggabungkannya, harus mengimplementasikan Closeableantarmuka.

kikibobo
sumber
3
@ Blororblade: Saya bertanya-tanya apakah Anda mungkin salah membaca jawaban kikibobo - tipe diri yang sifatnya memang memungkinkan Anda untuk membatasi jenis yang dapat mencampurnya, dan itu adalah bagian dari kegunaannya. Sebagai contoh, jika kita mendefinisikan trait A { self:B => ... }maka deklarasi X with Ahanya valid jika X memanjang B. Ya, Anda bisa mengatakan X with A with Q, di mana Q tidak memperpanjang B, tapi saya percaya poin kikibobo adalah bahwa X sangat dibatasi. Atau apakah saya melewatkan sesuatu?
AmigoNico
1
Terima kasih, kamu benar. Suara saya terkunci, tetapi untungnya saya bisa mengedit jawaban dan kemudian mengubah suara saya.
Blaisorblade
1

Pembaruan: Perbedaan utama adalah bahwa tipe-mandiri dapat bergantung pada beberapa kelas (saya akui itu adalah kasus sudut sedikit). Misalnya, Anda bisa memilikinya

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

Hal ini memungkinkan untuk menambahkan Employeemixin hanya untuk sesuatu yang merupakan subclass dari Persondan Expense. Tentu saja, ini hanya bermakna jika Expensediperluas Personatau sebaliknya. Intinya adalah bahwa menggunakan tipe diri Employeebisa independen dari hirarki kelas yang menjadi sandarannya. Tidak peduli apa yang memperpanjang apa - Jika Anda mengganti hierarki Expensevs Person, Anda tidak perlu memodifikasi Employee.

Petr Pudlák
sumber
Karyawan tidak perlu menjadi kelas untuk diturunkan dari Person. Ciri dapat memperpanjang kelas. Jika sifat Karyawan diperluas Orang alih-alih menggunakan tipe diri, contoh masih akan berfungsi. Saya menemukan contoh Anda menarik, tetapi sepertinya tidak menggambarkan use case untuk tipe diri.
Morgan Creighton
@MorganCreighton Cukup adil, saya tidak tahu bahwa sifat dapat memperpanjang kelas. Saya akan memikirkannya jika saya dapat menemukan contoh yang lebih baik.
Petr Pudlák
Ya, ini fitur bahasa yang mengejutkan. Jika sifat Karyawan kelas Karyawan yang diperluas, maka kelas apa pun yang pada akhirnya "mati", Karyawan juga harus memperpanjang Person. Tetapi pembatasan itu masih ada jika Karyawan menggunakan tipe diri alih-alih Orang yang diperluas. Ceria, Petr!
Morgan Creighton
1
Saya tidak mengerti mengapa "ini hanya bermakna jika Biaya memperluas Orang atau sebaliknya."
Robin Green
0

dalam kasus pertama, suatu sub-sifat atau sub-kelas B dapat dicampur ke dalam penggunaan apa pun A. Jadi B dapat menjadi sifat abstrak.

IttayD
sumber
Tidak, B dapat (dan memang) merupakan "sifat abstrak" dalam kedua kasus. Jadi tidak ada perbedaan dari perspektif itu.
Robin Green