Scala: Jenis abstrak vs generik

250

Saya sedang membaca A Tour of Scala: Abstract Types . Kapan lebih baik menggunakan tipe abstrak?

Sebagai contoh,

abstract class Buffer {
  type T
  val element: T
}

lebih tepatnya obat generik, misalnya,

abstract class Buffer[T] {
  val element: T
}
thatismatt
sumber

Jawaban:

257

Anda memiliki sudut pandang yang baik tentang masalah ini di sini:

Tujuan
Percakapan Tipe Sistem Scala dengan Martin Odersky, Bagian III
oleh Bill Venners dan Frank Sommers (18 Mei 2009)

Pembaruan (Oktober2009): apa yang berikut di bawah ini sebenarnya telah diilustrasikan dalam artikel baru ini oleh Bill Venners:
Anggota Tipe Abstrak versus Parameter Tipe Generik dalam Scala (lihat ringkasan di bagian akhir)


(Berikut adalah kutipan relevan dari wawancara pertama, Mei 2009, penekanan saya)

Prinsip umum

Selalu ada dua gagasan abstraksi:

  • parameterisasi dan
  • anggota abstrak.

Di Jawa Anda juga memiliki keduanya, tetapi itu tergantung pada apa yang Anda abstraksi.
Di Jawa Anda memiliki metode abstrak, tetapi Anda tidak bisa meneruskan metode sebagai parameter.
Anda tidak memiliki bidang abstrak, tetapi Anda bisa meneruskan nilai sebagai parameter.
Dan juga Anda tidak memiliki anggota tipe abstrak, tetapi Anda dapat menentukan tipe sebagai parameter.
Jadi di Jawa Anda juga memiliki ketiganya, tetapi ada perbedaan tentang prinsip abstraksi apa yang dapat Anda gunakan untuk hal-hal apa. Dan Anda bisa berpendapat bahwa perbedaan ini cukup sewenang-wenang.

Jalan Scala

Kami memutuskan untuk memiliki prinsip konstruksi yang sama untuk ketiga jenis anggota .
Jadi Anda bisa memiliki bidang abstrak serta parameter nilai.
Anda dapat melewatkan metode (atau "fungsi") sebagai parameter, atau Anda dapat abstrak darinya.
Anda dapat menentukan jenis sebagai parameter, atau Anda bisa abstrak di atasnya.
Dan apa yang kita dapatkan secara konseptual adalah bahwa kita dapat memodelkan satu dalam hal yang lain. Setidaknya pada prinsipnya, kita dapat mengekspresikan setiap jenis parameterisasi sebagai bentuk abstraksi berorientasi objek. Jadi bisa dibilang Scala adalah bahasa yang lebih orthogonal dan lengkap.

Mengapa?

Apa, khususnya, jenis abstrak yang membeli Anda adalah perawatan yang bagus untuk masalah kovarian yang telah kita bicarakan sebelumnya.
Satu masalah standar, yang sudah ada sejak lama, adalah masalah hewan dan makanan.
Teka-teki adalah memiliki kelas Animaldengan metode eat,, yang memakan makanan.
Masalahnya adalah jika kita subkelas Hewan dan memiliki kelas seperti Sapi, maka mereka akan makan hanya Rumput dan bukan makanan sembarangan. Seekor Sapi tidak bisa makan Ikan, misalnya.
Yang Anda inginkan adalah dapat mengatakan bahwa seekor sapi memiliki metode makan yang hanya memakan rumput dan bukan hal-hal lain.
Sebenarnya, Anda tidak dapat melakukan itu di Jawa karena ternyata Anda dapat membangun situasi yang tidak sehat, seperti masalah menetapkan Buah ke variabel Apple yang saya bicarakan sebelumnya.

Jawabannya adalah Anda menambahkan tipe abstrak ke dalam kelas Animal .
Anda berkata, kelas Hewan baru saya memiliki jenis SuitableFood, yang saya tidak tahu.
Jadi ini tipe abstrak. Anda tidak memberikan implementasi jenis itu. Maka Anda memiliki eatmetode yang hanya makan SuitableFood.
Dan kemudian di Cowkelas saya akan berkata, OK, saya punya Sapi, yang memperpanjang kelas Animal, dan untuk Cow type SuitableFood equals Grass.
Jadi tipe abstrak memberikan gagasan tentang tipe dalam superclass yang saya tidak tahu, yang kemudian saya isi kemudian dengan subkelas dengan sesuatu yang saya tahu .

Sama dengan parameterisasi?

Memang bisa. Anda bisa mengukur hewan kelas dengan jenis makanan yang dimakannya.
Tetapi dalam praktiknya, ketika Anda melakukannya dengan banyak hal yang berbeda, itu mengarah pada ledakan parameter , dan biasanya, terlebih lagi, dalam batas parameter .
Pada ECOOP 1998, Kim Bruce, Phil Wadler, dan saya memiliki sebuah makalah di mana kami menunjukkan bahwa ketika Anda meningkatkan jumlah hal yang tidak Anda ketahui, program tipikal akan tumbuh secara kuadrat .
Jadi ada alasan yang sangat bagus untuk tidak melakukan parameter, tetapi untuk memiliki anggota abstrak ini, karena mereka tidak memberi Anda ledakan kuadratik ini.


thatismatt bertanya dalam komentar:

Apakah Anda pikir yang berikut ini adalah ringkasan yang adil:

  • Tipe Abstrak digunakan dalam hubungan 'has-a' atau 'uses-a' (misalnya a Cow eats Grass)
  • di mana sebagai obat generik biasanya 'dari' hubungan (misalnya List of Ints)

Saya tidak yakin hubungannya berbeda antara menggunakan tipe abstrak atau generik. Yang berbeda adalah:

  • bagaimana mereka digunakan, dan
  • bagaimana batas parameter dikelola.

Untuk memahami apa yang dibicarakan Martin tentang "ledakan parameter, dan biasanya, terlebih lagi, dalam batas parameter ", dan pertumbuhan kuadratik berikutnya ketika tipe abstrak dimodelkan menggunakan generik, Anda dapat mempertimbangkan makalah " Abstraksi Komponen yang Dapat Diukur " "ditulis oleh ... Martin Odersky, dan Matthias Zenger untuk OOPSLA 2005, dirujuk dalam publikasi proyek Palcom (selesai pada 2007).

Ekstrak yang relevan

Definisi

Anggota tipe abstrak menyediakan cara yang fleksibel untuk abstrak dibandingkan jenis komponen beton.
Tipe abstrak dapat menyembunyikan informasi tentang internal komponen, mirip dengan penggunaannya dalam tanda tangan SML . Dalam kerangka kerja berorientasi objek di mana kelas dapat diperluas dengan pewarisan, mereka juga dapat digunakan sebagai sarana parameterisasi yang fleksibel (sering disebut polimorfisme keluarga, lihat entri weblog ini misalnya , dan makalah yang ditulis oleh Eric Ernst ).

(Catatan: Polimorfisme keluarga telah diusulkan untuk bahasa yang berorientasi objek sebagai solusi untuk mendukung kelas rekursif yang dapat digunakan kembali namun aman jenisnya.
Gagasan utama polimorfisme keluarga adalah gagasan keluarga, yang digunakan untuk mengelompokkan kelas yang saling rekursif)

abstraksi tipe terbatas

abstract class MaxCell extends AbsCell {
type T <: Ordered { type O = T }
def setMax(x: T) = if (get < x) set(x)
}

Di sini, deklarasi tipe T dibatasi oleh batas tipe atas yang terdiri dari nama kelas Dipesan dan penyempurnaan { type O = T }.
Membatasi batas atas spesialisasi T di subclass dengan yang subtipe dari Memerintahkan dimana anggota jenis Odari equals T.
Karena kendala ini, <metode class Ordered dijamin berlaku untuk penerima dan argumen tipe T.
Contoh menunjukkan bahwa anggota tipe terikat itu sendiri dapat muncul sebagai bagian dari batas.
(yaitu Scala mendukung polimorfisme terbatas-F )

(Catatan, dari Peter Canning, William Cook, Walter Hill, makalah Walter Olthoff:
Kuantifikasi terbatas diperkenalkan oleh Cardelli dan Wegner sebagai alat untuk mengetik fungsi yang beroperasi secara seragam pada semua subtipe dari jenis tertentu.
Mereka mendefinisikan model "objek" sederhana. dan menggunakan kuantifikasi terbatas untuk fungsi pemeriksaan tipe yang masuk akal pada semua objek yang memiliki set "atribut" tertentu.
Presentasi yang lebih realistis dari bahasa berorientasi objek akan memungkinkan objek yang merupakan elemen dari tipe yang didefinisikan secara rekursif .
Dalam konteks ini, dibatasi kuantifikasi tidak lagi memenuhi tujuannya, mudah untuk menemukan fungsi yang masuk akal pada semua objek yang memiliki serangkaian metode tertentu, tetapi yang tidak dapat diketik dalam sistem Cardelli-Wegner.
Untuk memberikan dasar untuk fungsi polimorfik yang diketik dalam bahasa berorientasi objek, kami memperkenalkan kuantifikasi terikat-F)

Dua wajah dari koin yang sama

Ada dua bentuk utama abstraksi dalam bahasa pemrograman:

  • parameterisasi dan
  • anggota abstrak.

Bentuk pertama adalah tipikal untuk bahasa fungsional, sedangkan bentuk kedua biasanya digunakan dalam bahasa berorientasi objek.

Secara tradisional, Java mendukung parameterisasi untuk nilai, dan abstraksi anggota untuk operasi. Java 5.0 yang lebih baru dengan generik mendukung parameterisasi juga untuk tipe.

Argumen untuk menyertakan obat generik dalam Scala ada dua:

  • Pertama, pengkodean menjadi tipe abstrak tidak mudah dilakukan dengan tangan. Selain kerugian dalam keringkasan, ada juga masalah konflik nama yang tidak disengaja antara nama tipe abstrak yang meniru parameter tipe.

  • Kedua, generik dan tipe abstrak biasanya memiliki peran berbeda dalam program Scala.

    • Generik biasanya digunakan ketika seseorang hanya perlu mengetikkan contoh , sedangkan
    • tipe abstrak biasanya digunakan ketika seseorang perlu merujuk ke tipe abstrak dari kode klien .
      Yang terakhir muncul khususnya dalam dua situasi:
    • Seseorang mungkin ingin menyembunyikan definisi yang tepat dari anggota tipe dari kode klien, untuk mendapatkan semacam enkapsulasi yang diketahui dari sistem modul SML-style.
    • Atau seseorang mungkin ingin menimpa tipe kovarian dalam subclass untuk mendapatkan polimorfisme keluarga.

Dalam sistem dengan polimorfisme terbatas, menulis ulang tipe abstrak menjadi generik mungkin memerlukan ekspansi kuadrat dari tipe bound .


Perbarui Oktober 2009

Abstrak Anggota Tipe versus Parameter Tipe Generik dalam Scala (Bill Venners)

(penekanan milikku)

Pengamatan saya sejauh ini tentang anggota tipe abstrak adalah bahwa mereka terutama merupakan pilihan yang lebih baik daripada parameter tipe umum ketika:

  • Anda ingin orang-orang bercampur dalam definisi tipe-tipe itu melalui ciri-ciri .
  • Anda berpikir penyebutan nama anggota tipe secara eksplisit ketika sedang didefinisikan akan membantu pembacaan kode .

Contoh:

jika Anda ingin melewatkan tiga objek fixture yang berbeda ke dalam tes, Anda dapat melakukannya, tetapi Anda harus menentukan tiga jenis, satu untuk setiap parameter. Jadi seandainya saya mengambil pendekatan parameter type, kelas-kelas suite Anda mungkin berakhir seperti ini:

// Type parameter version
class MySuite extends FixtureSuite3[StringBuilder, ListBuffer, Stack] with MyHandyFixture {
  // ...
}

Sedangkan dengan pendekatan tipe anggota akan terlihat seperti ini:

// Type member version
class MySuite extends FixtureSuite3 with MyHandyFixture {
  // ...
}

Satu perbedaan kecil lainnya antara anggota tipe abstrak dan parameter tipe generik adalah bahwa ketika parameter tipe generik ditentukan, pembaca kode tidak melihat nama parameter tipe. Demikianlah seseorang untuk melihat baris kode ini:

// Type parameter version
class MySuite extends FixtureSuite[StringBuilder] with StringBuilderFixture {
  // ...
}

Mereka tidak akan tahu apa nama parameter tipe yang ditentukan sebagai StringBuilder tanpa mencarinya. Sedangkan nama parameter tipe ada di sana dalam kode dalam pendekatan anggota tipe abstrak:

// Type member version
class MySuite extends FixtureSuite with StringBuilderFixture {
  type FixtureParam = StringBuilder
  // ...
}

Dalam kasus terakhir, pembaca kode dapat melihat bahwa itu StringBuilderadalah tipe "parameter fixture".
Mereka masih perlu mencari tahu apa yang dimaksud dengan "parameter fixture", tetapi mereka setidaknya bisa mendapatkan nama tipe tanpa melihat dalam dokumentasi.

VONC
sumber
61
Bagaimana saya bisa mendapatkan poin karma dengan menjawab pertanyaan Scala ketika Anda datang dan melakukan ini ??? :-)
Daniel C. Sobral
7
Hai Daniel: Saya pikir harus ada contoh konkret untuk menggambarkan keunggulan tipe abstrak daripada parametrization. Memposting beberapa di utas ini akan menjadi awal yang baik;) Saya tahu saya akan membatalkan itu.
VonC
1
Apakah Anda pikir yang berikut ini adalah ringkasan yang adil: Abstrak Jenis digunakan dalam hubungan 'has-a' atau 'uses-a' (misalnya Rumput Makan Sapi) di mana sebagai generik biasanya hubungan 'dari' (misalnya Daftar Ints)
thatismatt
Saya tidak yakin hubungannya berbeda antara menggunakan tipe abstrak atau generik. Yang berbeda adalah bagaimana mereka digunakan, dan bagaimana batas parameter dikelola. Lebih dalam jawaban saya sebentar lagi.
VonC
1
Catatan untuk diri sendiri: lihat juga posting blog Mei 2010 ini: daily-scala.blogspot.com/2010/05/…
VonC
37

Saya memiliki pertanyaan yang sama ketika saya membaca tentang Scala.

Keuntungan menggunakan obat generik adalah Anda membuat keluarga jenis. Tak seorang pun akan perlu subclass Buffer-mereka hanya dapat menggunakan Buffer[Any], Buffer[String], dll

Jika Anda menggunakan tipe abstrak, maka orang-orang akan dipaksa untuk membuat subkelas. Orang akan membutuhkan kelas seperti AnyBuffer, StringBuffer, dll

Anda perlu memutuskan mana yang lebih baik untuk kebutuhan khusus Anda.

Daniel Yankowsky
sumber
18
mmm menipis banyak meningkat di bagian depan ini, Anda hanya dapat memerlukan Buffer { type T <: String }atau Buffer { type T = String }tergantung pada kebutuhan Anda
Eduardo Pareja Tobes
21

Anda dapat menggunakan tipe abstrak bersama dengan parameter tipe untuk membuat templat kustom.

Mari kita asumsikan Anda perlu membuat pola dengan tiga sifat yang terhubung:

trait AA[B,C]
trait BB[C,A]
trait CC[A,B]

dengan cara argumen yang disebutkan dalam parameter tipe adalah AA, BB, CC itu sendiri dengan hormat

Anda dapat datang dengan beberapa jenis kode:

trait AA[B<:BB[C,AA[B,C]],C<:CC[AA[B,C],B]]
trait BB[C<:CC[A,BB[C,A]],A<:AA[BB[C,A],C]]
trait CC[A<:AA[B,CC[A,B]],B<:BB[CC[A,B],A]]

yang tidak akan bekerja dengan cara sederhana ini karena jenis ikatan parameter. Anda harus menjadikannya kovarian untuk mewarisi dengan benar

trait AA[+B<:BB[C,AA[B,C]],+C<:CC[AA[B,C],B]]
trait BB[+C<:CC[A,BB[C,A]],+A<:AA[BB[C,A],C]]
trait CC[+A<:AA[B,CC[A,B]],+B<:BB[CC[A,B],A]]

Sampel yang satu ini akan dikompilasi tetapi menetapkan persyaratan kuat pada aturan varians dan tidak dapat digunakan dalam beberapa kesempatan

trait AA[+B<:BB[C,AA[B,C]],+C<:CC[AA[B,C],B]] {
  def forth(x:B):C
  def back(x:C):B
}
trait BB[+C<:CC[A,BB[C,A]],+A<:AA[BB[C,A],C]] {
  def forth(x:C):A
  def back(x:A):C
}
trait CC[+A<:AA[B,CC[A,B]],+B<:BB[CC[A,B],A]] {
  def forth(x:A):B
  def back(x:B):A
}

Compiler akan keberatan dengan banyak kesalahan pemeriksaan varians

Dalam hal ini, Anda dapat mengumpulkan semua persyaratan tipe dalam sifat tambahan dan menetapkan sifat-sifat lain di atasnya

//one trait to rule them all
trait OO[O <: OO[O]] { this : O =>
  type A <: AA[O]
  type B <: BB[O]
  type C <: CC[O]
}
trait AA[O <: OO[O]] { this : O#A =>
  type A = O#A
  type B = O#B
  type C = O#C
  def left(l:B):C
  def right(r:C):B = r.left(this)
  def join(l:B, r:C):A
  def double(l:B, r:C):A = this.join( l.join(r,this), r.join(this,l) )
}
trait BB[O <: OO[O]] { this : O#B =>
  type A = O#A
  type B = O#B
  type C = O#C
  def left(l:C):A
  def right(r:A):C = r.left(this)
  def join(l:C, r:A):B
  def double(l:C, r:A):B = this.join( l.join(r,this), r.join(this,l) )
}
trait CC[O <: OO[O]] { this : O#C =>
  type A = O#A
  type B = O#B
  type C = O#C
  def left(l:A):B
  def right(r:B):A = r.left(this)
  def join(l:A, r:B):C
  def double(l:A, r:B):C = this.join( l.join(r,this), r.join(this,l) )
}

Sekarang kita dapat menulis representasi konkret untuk pola yang dijelaskan, mendefinisikan metode kiri dan bergabung di semua kelas dan dapatkan benar dan gandakan secara gratis

class ReprO extends OO[ReprO] {
  override type A = ReprA
  override type B = ReprB
  override type C = ReprC
}
case class ReprA(data : Int) extends AA[ReprO] {
  override def left(l:B):C = ReprC(data - l.data)
  override def join(l:B, r:C) = ReprA(l.data + r.data)
}
case class ReprB(data : Int) extends BB[ReprO] {
  override def left(l:C):A = ReprA(data - l.data)
  override def join(l:C, r:A):B = ReprB(l.data + r.data)
}
case class ReprC(data : Int) extends CC[ReprO] {
  override def left(l:A):B = ReprB(data - l.data)
  override def join(l:A, r:B):C = ReprC(l.data + r.data)
}

Jadi, tipe dan parameter tipe abstrak digunakan untuk membuat abstraksi. Keduanya memiliki kelebihan dan kekurangan. Tipe abstrak lebih spesifik dan mampu menggambarkan struktur tipe apa pun tetapi verbose dan perlu ditentukan secara eksplisit. Ketik parameter dapat membuat banyak jenis secara instan tetapi memberi Anda kekhawatiran tambahan tentang pewarisan dan mengetik batas.

Mereka memberikan sinergi satu sama lain dan dapat digunakan bersama untuk membuat abstraksi kompleks yang tidak dapat diungkapkan hanya dengan satu di antaranya.

ayvango
sumber
0

Saya pikir tidak ada banyak perbedaan di sini. Tipe anggota abstrak dapat dilihat sebagai tipe eksistensial yang mirip dengan tipe rekaman dalam beberapa bahasa fungsional lainnya.

Sebagai contoh, kami memiliki:

class ListT {
  type T
  ...
}

dan

class List[T] {...}

Maka ListTsama saja dengan List[_]. Keyakinan anggota tipe adalah bahwa kita dapat menggunakan kelas tanpa tipe konkret eksplisit dan menghindari terlalu banyak parameter tipe.

Comcx
sumber