Konversi implisit vs. kelas jenis

94

Di Scala, kita dapat menggunakan setidaknya dua metode untuk retrofit tipe yang sudah ada atau yang baru. Misalkan kita ingin menyatakan bahwa sesuatu dapat dikuantifikasi menggunakan Int. Kita dapat mendefinisikan sifat berikut.

Konversi implisit

trait Quantifiable{ def quantify: Int }

Dan kemudian kita dapat menggunakan konversi implisit untuk mengukur misalnya String dan Daftar.

implicit def string2quant(s: String) = new Quantifiable{ 
  def quantify = s.size 
}
implicit def list2quantifiable[A](l: List[A]) = new Quantifiable{ 
  val quantify = l.size 
}

Setelah mengimpor ini, kita dapat memanggil metode quantifypada string dan daftar. Perhatikan bahwa daftar yang dapat diukur menyimpan panjangnya, sehingga menghindari traversal daftar yang mahal pada panggilan berikutnya ke quantify.

Ketik kelas

Alternatifnya adalah dengan mendefinisikan "saksi" Quantified[A]yang menyatakan, bahwa beberapa tipe Adapat dikuantifikasi.

trait Quantified[A] { def quantify(a: A): Int }

Kami kemudian menyediakan contoh kelas jenis ini untuk Stringdan di Listsuatu tempat.

implicit val stringQuantifiable = new Quantified[String] {
  def quantify(s: String) = s.size 
}

Dan jika kita kemudian menulis metode yang perlu mengukur argumennya, kita menulis:

def sumQuantities[A](as: List[A])(implicit ev: Quantified[A]) = 
  as.map(ev.quantify).sum

Atau menggunakan sintaks terikat konteks:

def sumQuantities[A: Quantified](as: List[A]) = 
  as.map(implicitly[Quantified[A]].quantify).sum

Tetapi kapan harus menggunakan metode yang mana?

Sekarang pertanyaannya. Bagaimana saya bisa memutuskan di antara kedua konsep itu?

Apa yang saya perhatikan sejauh ini.

jenis kelas

  • kelas tipe memungkinkan sintaks terikat konteks yang bagus
  • dengan kelas tipe saya tidak membuat objek pembungkus baru pada setiap penggunaan
  • sintaks konteks terikat tidak berfungsi lagi jika kelas tipe memiliki beberapa parameter tipe; bayangkan saya ingin mengukur hal-hal tidak hanya dengan bilangan bulat tetapi dengan nilai-nilai dari beberapa tipe umum T. Saya ingin membuat kelas tipeQuantified[A,T]

konversi implisit

  • sejak saya membuat objek baru, saya dapat menyimpan nilai di sana atau menghitung representasi yang lebih baik; tetapi haruskah saya menghindari ini, karena ini mungkin terjadi beberapa kali dan konversi eksplisit mungkin hanya akan dipanggil sekali?

Apa yang saya harapkan dari sebuah jawaban

Sajikan satu (atau lebih) kasus penggunaan di mana perbedaan antara kedua konsep itu penting dan jelaskan mengapa saya lebih memilih salah satu daripada yang lain. Juga menjelaskan esensi dari kedua konsep dan hubungannya satu sama lain akan menyenangkan, bahkan tanpa contoh.

ziggystar
sumber
Ada beberapa kebingungan dalam poin kelas tipe di mana Anda menyebutkan "terikat tampilan", meskipun kelas tipe menggunakan batas konteks.
Daniel C. Sobral
1
+1 pertanyaan bagus; Saya sangat tertarik dengan jawaban menyeluruh untuk ini.
Dan Burton
@Daniel Terima kasih. Saya selalu salah.
ziggystar
2
Anda keliru dalam satu tempat: di kedua contoh konversi implisit Anda Anda menyimpan sizedari daftar nilai dan mengatakan bahwa ia menghindari traversal mahal dari daftar panggilan berikutnya untuk dihitung, tetapi dari setiap panggilan Anda ke quantifydalam list2quantifiableakan memicu lagi-lagi dengan demikian mengembalikan Quantifiabledan menghitung ulang quantifyproperti. Apa yang saya katakan adalah bahwa sebenarnya tidak ada cara untuk menyimpan hasil dengan konversi implisit.
Nikita Volkov
@NikitaVolkov Pengamatan Anda benar. Dan saya membahas ini dalam pertanyaan saya di paragraf kedua hingga terakhir. Caching berfungsi, ketika objek yang dikonversi digunakan lebih lama setelah satu panggilan metode konversi (dan mungkin diteruskan dalam bentuk yang dikonversi). Sementara kelas tipe mungkin akan dirantai di sepanjang objek yang belum dikonversi saat masuk lebih dalam.
ziggystar

Jawaban:

42

Meskipun saya tidak ingin menduplikasi materi saya dari Scala In Depth , saya pikir perlu dicatat bahwa tipe kelas / sifat tipe jauh lebih fleksibel.

def foo[T: TypeClass](t: T) = ...

memiliki kemampuan untuk mencari lingkungan lokalnya untuk kelas tipe default. Namun, saya dapat mengganti perilaku default kapan saja dengan salah satu dari dua cara berikut:

  1. Membuat / mengimpor instance kelas tipe implisit di Scope ke pencarian implisit sirkuit pendek
  2. Melewati kelas tipe secara langsung

Berikut contohnya:

def myMethod(): Unit = {
   // overrides default implicit for Int
   implicit object MyIntFoo extends Foo[Int] { ... }
   foo(5)
   foo(6) // These all use my overridden type class
   foo(7)(new Foo[Int] { ... }) // This one needs a different configuration
}

Ini membuat kelas tipe jauh lebih fleksibel. Hal lain adalah bahwa jenis kelas / sifat mendukung pencarian implisit dengan lebih baik.

Dalam contoh pertama Anda, jika Anda menggunakan tampilan implisit, kompilator akan melakukan pencarian implisit untuk:

Function1[Int, ?]

Yang akan melihat Function1objek pendamping dan Intobjek pendamping.

Perhatikan bahwa Quantifiabletidak ada dalam pencarian implisit. Ini berarti Anda harus menempatkan tampilan implisit dalam objek paket atau mengimpornya ke dalam cakupan. Lebih banyak pekerjaan untuk mengingat apa yang sedang terjadi.

Di sisi lain, kelas tipe bersifat eksplisit . Anda melihat apa yang dicari di tanda tangan metode. Anda juga memiliki pencarian implisit

Quantifiable[Int]

yang akan mencari di Quantifiableobjek pendamping dan Int objek pendamping. Artinya Anda dapat memberikan default dan tipe baru (seperti MyStringkelas) dapat menyediakan default dalam objek pendampingnya dan akan dicari secara implisit.

Secara umum, saya menggunakan kelas tipe. Mereka jauh lebih fleksibel untuk contoh awal. Satu-satunya tempat saya menggunakan konversi implisit adalah saat menggunakan lapisan API antara pembungkus Scala dan pustaka Java, dan bahkan ini bisa 'berbahaya' jika Anda tidak berhati-hati.

jsuereth
sumber
20

Satu kriteria yang dapat diterapkan adalah bagaimana Anda ingin fitur baru "terasa"; menggunakan konversi implisit, Anda dapat membuatnya terlihat seperti metode lain:

"my string".newFeature

... saat menggunakan kelas tipe, akan selalu terlihat seperti Anda memanggil fungsi eksternal:

newFeature("my string")

Satu hal yang bisa Anda capai dengan kelas tipe dan bukan dengan konversi implisit adalah menambahkan properti ke sebuah tipe , daripada ke sebuah instance tipe. Anda kemudian dapat mengakses properti ini bahkan ketika Anda tidak memiliki contoh dari tipe yang tersedia. Contoh kanonisnya adalah:

trait Default[T] { def value : T }

implicit object DefaultInt extends Default[Int] {
  def value = 42
}

implicit def listsHaveDefault[T : Default] = new Default[List[T]] {
  def value = implicitly[Default[T]].value :: Nil
}

def default[T : Default] = implicitly[Default[T]].value

scala> default[List[List[Int]]]
resN: List[List[Int]] = List(List(42))

Contoh ini juga menunjukkan bagaimana konsep terkait erat: kelas tipe hampir tidak akan berguna jika tidak ada mekanisme untuk menghasilkan banyak instansinya secara tak terbatas; tanpa implicitmetode (bukan konversi, harus diakui), saya hanya dapat memiliki banyak jenis Defaultproperti.

Philippe
sumber
@ Phillippe - Saya sangat tertarik dengan teknik yang Anda tulis ... tetapi tampaknya tidak berhasil pada Scala 2.11.6. Saya memposting pertanyaan yang meminta pembaruan pada jawaban Anda. terima kasih sebelumnya jika Anda dapat membantu: Silakan lihat: stackoverflow.com/questions/31910923/…
Chris Bedford
@ChrisBedford Saya menambahkan definisi defaultuntuk pembaca mendatang.
Philippe
13

Anda dapat memikirkan perbedaan antara kedua teknik dengan analogi aplikasi fungsi, hanya dengan pembungkus bernama. Sebagai contoh:

trait Foo1[A] { def foo(a: A): Int }  // analogous to A => Int
trait Foo0    { def foo: Int }        // analogous to Int

Sebuah instance dari yang pertama merangkum sebuah fungsi dari sebuah tipe A => Int, sedangkan sebuah instance dari yang terakhir telah diterapkan ke sebuah A. Anda bisa melanjutkan polanya ...

trait Foo2[A, B] { def foo(a: A, b: B): Int } // sort of like A => B => Int

sehingga Anda dapat berpikir Foo1[B]seperti aplikasi parsial Foo2[A, B]untuk beberapa Acontoh. Sebuah contoh yang bagus tentang ini ditulis oleh Miles Sabin sebagai "Ketergantungan Fungsional di Scala" .

Jadi sebenarnya maksud saya adalah, pada prinsipnya:

  • "pimping" kelas (melalui konversi implisit) adalah kasus "urutan nol" ...
  • mendeklarasikan kelas tipe adalah kasus "urutan pertama" ...
  • kelas tipe multi-parameter dengan fundeps (atau sesuatu seperti fundeps) adalah kasus umum.
mergeconflict
sumber