Di mana Scala mencari implisit?

398

Sebuah implisit pertanyaan untuk pendatang baru untuk Scala tampaknya menjadi: di mana melakukan compiler mencari implicits? Maksud saya implisit karena pertanyaannya sepertinya tidak pernah terbentuk sepenuhnya, seolah-olah tidak ada kata-kata untuk itu. :-) Misalnya, dari mana nilai-nilai untuk di integralbawah ini berasal?

scala> import scala.math._
import scala.math._

scala> def foo[T](t: T)(implicit integral: Integral[T]) {println(integral)}
foo: [T](t: T)(implicit integral: scala.math.Integral[T])Unit

scala> foo(0)
scala.math.Numeric$IntIsIntegral$@3dbea611

scala> foo(0L)
scala.math.Numeric$LongIsIntegral$@48c610af

Pertanyaan lain yang tidak menindaklanjuti mereka yang memutuskan untuk mempelajari jawaban atas pertanyaan pertama adalah bagaimana kompiler memilih mana yang tersirat untuk digunakan, dalam situasi tertentu yang ambiguitas nyata (tetapi itu tetap kompilasi)?

Misalnya, scala.Predefmendefinisikan dua konversi dari String: satu ke yang WrappedStringlain ke StringOps. Kedua kelas, bagaimanapun, berbagi banyak metode, jadi mengapa Scala tidak mengeluh tentang ambiguitas ketika, misalnya, menelepon map?

Catatan: pertanyaan ini terinspirasi oleh pertanyaan lain ini , dengan harapan menyatakan masalah secara lebih umum. Contoh itu disalin dari sana, karena itu disebut dalam jawabannya.

Daniel C. Sobral
sumber

Jawaban:

554

Jenis-Jenis Implikasinya

Implicits dalam Scala mengacu pada nilai yang dapat dilewati "secara otomatis", sehingga, atau konversi dari satu jenis ke yang lain yang dibuat secara otomatis.

Konversi Tersirat

Berbicara sangat singkat tentang tipe yang terakhir, jika seseorang memanggil metode mpada objek okelas C, dan kelas itu tidak mendukung metode m, maka Scala akan mencari konversi implisit dari Cke sesuatu yang memang mendukung m. Contoh sederhana akan menjadi metode mappada String:

"abc".map(_.toInt)

Stringtidak mendukung metode ini map, tetapi mendukung StringOps, dan ada konversi tersirat dari Stringmenjadi StringOpstersedia (lihat implicit def augmentStringdi Predef).

Parameter Tersirat

Jenis implisit lainnya adalah parameter implisit . Ini diteruskan ke pemanggilan metode seperti parameter lainnya, tetapi kompiler mencoba mengisinya secara otomatis. Jika tidak bisa, itu akan mengeluh. Seseorang dapat melewatkan parameter-parameter ini secara eksplisit, seperti itulah yang digunakan seseorang breakOut, misalnya (lihat pertanyaan tentang breakOut, pada hari Anda merasa tertantang).

Dalam hal ini, seseorang harus menyatakan perlunya implisit, seperti foodeklarasi metode:

def foo[T](t: T)(implicit integral: Integral[T]) {println(integral)}

Lihat Batas

Ada satu situasi di mana sebuah implisit adalah konversi implisit dan parameter implisit. Sebagai contoh:

def getIndex[T, CC](seq: CC, value: T)(implicit conv: CC => Seq[T]) = seq.indexOf(value)

getIndex("abc", 'a')

Metode ini getIndexdapat menerima objek apa pun, asalkan ada konversi implisit yang tersedia dari kelasnya ke Seq[T]. Karena itu, saya dapat meneruskan Stringke getIndex, dan itu akan berhasil.

Di belakang layar, kompiler berubah seq.IndexOf(value)menjadi conv(seq).indexOf(value).

Ini sangat berguna sehingga ada gula sintaksis untuk menuliskannya. Menggunakan gula sintaksis ini, getIndexdapat didefinisikan seperti ini:

def getIndex[T, CC <% Seq[T]](seq: CC, value: T) = seq.indexOf(value)

Gula sintaksis ini digambarkan sebagai pandangan terikat , mirip dengan batas atas ( CC <: Seq[Int]) atau batas bawah ( T >: Null).

Batas Konteks

Pola umum lainnya dalam parameter implisit adalah pola kelas tipe . Pola ini memungkinkan penyediaan antarmuka umum ke kelas yang tidak mendeklarasikannya. Keduanya dapat berfungsi sebagai pola jembatan - mendapatkan pemisahan kekhawatiran - dan sebagai pola adaptor.

The Integralkelas yang Anda sebutkan adalah contoh klasik dari pola jenis kelas. Contoh lain tentang perpustakaan standar Scala adalah Ordering. Ada perpustakaan yang banyak menggunakan pola ini, yang disebut Scalaz.

Ini adalah contoh penggunaannya:

def sum[T](list: List[T])(implicit integral: Integral[T]): T = {
    import integral._   // get the implicits in question into scope
    list.foldLeft(integral.zero)(_ + _)
}

Ada juga gula sintaksis untuk itu, yang disebut konteks terikat , yang dibuat kurang bermanfaat oleh kebutuhan untuk merujuk pada implisit. Konversi langsung dari metode itu terlihat seperti ini:

def sum[T : Integral](list: List[T]): T = {
    val integral = implicitly[Integral[T]]
    import integral._   // get the implicits in question into scope
    list.foldLeft(integral.zero)(_ + _)
}

Batas konteks lebih berguna ketika Anda hanya perlu meneruskannya ke metode lain yang menggunakannya. Sebagai contoh, metode sortedpada Seqperlu implisit Ordering. Untuk membuat metode reverseSort, seseorang dapat menulis:

def reverseSort[T : Ordering](seq: Seq[T]) = seq.sorted.reverse

Karena Ordering[T]secara implisit diteruskan ke reverseSort, maka dapat diteruskan secara implisit kepada sorted.

Dari mana Implik berasal?

Ketika kompilator melihat perlunya implisit, baik karena Anda memanggil metode yang tidak ada pada kelas objek, atau karena Anda memanggil metode yang membutuhkan parameter implisit, ia akan mencari implisit yang sesuai dengan kebutuhan. .

Pencarian ini mematuhi aturan tertentu yang menentukan yang tersirat terlihat dan yang tidak. Tabel berikut menunjukkan di mana kompiler akan mencari implisit diambil dari presentasi yang sangat baik tentang implisit oleh Josh Suereth, yang saya sungguh-sungguh merekomendasikan kepada siapa pun yang ingin meningkatkan pengetahuan Scala mereka. Sejak itu telah dilengkapi dengan umpan balik dan pembaruan.

Implisit yang tersedia pada nomor 1 di bawah ini lebih diutamakan daripada yang di bawah angka 2. Selain itu, jika ada beberapa argumen yang memenuhi syarat yang sesuai dengan tipe parameter implisit, yang paling spesifik akan dipilih menggunakan aturan resolusi kelebihan beban statis (lihat Scala Spesifikasi §6.26.3). Informasi lebih rinci dapat ditemukan dalam pertanyaan yang saya tautkan pada akhir jawaban ini.

  1. Tampilan pertama dalam lingkup saat ini
    • Implikasi didefinisikan dalam ruang lingkup saat ini
    • Impor eksplisit
    • impor wildcard
    • Cakupan yang sama di file lain
  2. Sekarang lihat tipe terkait di
    • Objek pendamping dari suatu tipe
    • Lingkup implisit dari tipe argumen (2.9.1)
    • Lingkup argumen tipe implisit (2.8.0)
    • Benda luar untuk tipe bersarang
    • Dimensi lainnya

Mari kita berikan beberapa contoh untuk mereka:

Implikasi Didefinisikan dalam Lingkup Saat Ini

implicit val n: Int = 5
def add(x: Int)(implicit y: Int) = x + y
add(5) // takes n from the current scope

Impor Eksplisit

import scala.collection.JavaConversions.mapAsScalaMap
def env = System.getenv() // Java map
val term = env("TERM")    // implicit conversion from Java Map to Scala Map

Impor Kartu Liar

def sum[T : Integral](list: List[T]): T = {
    val integral = implicitly[Integral[T]]
    import integral._   // get the implicits in question into scope
    list.foldLeft(integral.zero)(_ + _)
}

Lingkup yang Sama di File Lain

Sunting : Sepertinya ini tidak memiliki prioritas yang berbeda. Jika Anda memiliki beberapa contoh yang menunjukkan perbedaan yang diutamakan, silakan berikan komentar. Kalau tidak, jangan mengandalkan yang ini.

Ini seperti contoh pertama, tetapi mengasumsikan definisi implisit dalam file yang berbeda dari penggunaannya. Lihat juga bagaimana objek paket dapat digunakan untuk memasukkan implisit.

Objek Pendamping Jenis

Ada dua objek sahabat catatan di sini. Pertama, objek pendamping dari jenis "sumber" diperiksa. Misalnya, di dalam objek Optionada konversi implisit ke Iterable, sehingga orang dapat memanggil Iterablemetode Option, atau meneruskan Optionke sesuatu yang mengharapkan suatu Iterable. Sebagai contoh:

for {
    x <- List(1, 2, 3)
    y <- Some('x')
} yield (x, y)

Ungkapan itu diterjemahkan oleh kompiler ke

List(1, 2, 3).flatMap(x => Some('x').map(y => (x, y)))

Namun, List.flatMapmengharapkan TraversableOnce, yang Optiontidak. Kompilator kemudian melihat ke dalam Optionobjek pendamping dan menemukan konversi ke Iterable, yang merupakan TraversableOnce, membuat ungkapan ini benar.

Kedua, objek pendamping dari tipe yang diharapkan:

List(1, 2, 3).sorted

Metode ini sortedmengambil implisit Ordering. Dalam hal ini, terlihat di dalam objek Ordering, pendamping kelas Ordering, dan menemukan implisit di Ordering[Int]sana.

Perhatikan bahwa objek pengiring dari kelas super juga diperiksa. Sebagai contoh:

class A(val n: Int)
object A { 
    implicit def str(a: A) = "A: %d" format a.n
}
class B(val x: Int, y: Int) extends A(y)
val b = new B(5, 2)
val s: String = b  // s == "A: 2"

Beginilah Scala menemukan yang tersirat Numeric[Int]dan Numeric[Long]dalam pertanyaan Anda, omong-omong, seperti yang ditemukan di dalam Numeric, bukan Integral.

Lingkup Implisit dari Jenis Argumen

Jika Anda memiliki metode dengan tipe argumen A, maka cakupan tipe implisit Ajuga akan dipertimbangkan. Dengan "ruang lingkup implisit" Maksud saya bahwa semua aturan ini akan diterapkan secara berulang - misalnya, objek pendamping dari Aakan dicari implisit, seperti aturan di atas.

Perhatikan bahwa ini tidak berarti ruang lingkup implisit dari Aakan dicari konversi dari parameter itu, tetapi dari keseluruhan ekspresi. Sebagai contoh:

class A(val n: Int) {
  def +(other: A) = new A(n + other.n)
}
object A {
  implicit def fromInt(n: Int) = new A(n)
}

// This becomes possible:
1 + new A(1)
// because it is converted into this:
A.fromInt(1) + new A(1)

Ini tersedia sejak Scala 2.9.1.

Lingkup Argumen Jenis Tersirat

Ini diperlukan untuk membuat pola kelas tipe benar-benar berfungsi. Pertimbangkan Ordering, misalnya: Muncul dengan beberapa implisit dalam objek pendampingnya, tetapi Anda tidak dapat menambahkan barang ke sana. Jadi bagaimana Anda bisa membuat Orderinguntuk kelas Anda sendiri yang secara otomatis ditemukan?

Mari kita mulai dengan implementasinya:

class A(val n: Int)
object A {
    implicit val ord = new Ordering[A] {
        def compare(x: A, y: A) = implicitly[Ordering[Int]].compare(x.n, y.n)
    }
}

Jadi, pertimbangkan apa yang terjadi ketika Anda menelepon

List(new A(5), new A(2)).sorted

Seperti yang kita lihat, metode sortedmengharapkan Ordering[A](sebenarnya, mengharapkan Ordering[B], di mana B >: A). Tidak ada hal seperti itu di dalam Ordering, dan tidak ada jenis "sumber" yang akan dilihat. Jelas, itu adalah menemukan di dalam A, yang merupakan jenis argumen dari Ordering.

Ini juga bagaimana berbagai metode pengumpulan mengharapkan CanBuildFrompekerjaan: implisit ditemukan di dalam objek pengiring ke parameter tipe CanBuildFrom.

Catatan : Orderingdidefinisikan sebagai trait Ordering[T], di mana Tparameter tipe. Sebelumnya, saya mengatakan bahwa Scala melihat ke dalam parameter tipe, yang tidak masuk akal. Implisit mencari di atas adalah Ordering[A], di mana Amerupakan tipe yang sebenarnya, bukan jenis parameter: itu adalah argumen tipe untuk Ordering. Lihat bagian 7.2 dari spesifikasi Scala.

Ini tersedia sejak Scala 2.8.0.

Objek Luar untuk Jenis Bertingkat

Saya belum benar-benar melihat contoh ini. Saya akan berterima kasih jika seseorang dapat membagikannya. Prinsipnya sederhana:

class A(val n: Int) {
  class B(val m: Int) { require(m < n) }
}
object A {
  implicit def bToString(b: A#B) = "B: %d" format b.m
}
val a = new A(5)
val b = new a.B(3)
val s: String = b  // s == "B: 3"

Dimensi Lainnya

Saya cukup yakin ini hanya lelucon, tetapi jawaban ini mungkin tidak terkini. Jadi jangan anggap pertanyaan ini sebagai wasit terakhir dari apa yang terjadi, dan jika Anda tahu itu sudah ketinggalan zaman, mohon informasikan kepada saya sehingga saya dapat memperbaikinya.

EDIT

Pertanyaan terkait yang menarik:

Daniel C. Sobral
sumber
60
Sudah waktunya bagi Anda untuk mulai menggunakan jawaban Anda dalam sebuah buku, sekarang hanya tinggal menyatukannya.
pedrofurla
3
@pedrofurla Saya telah dianggap menulis buku dalam bahasa Portugis. Jika seseorang dapat menemukan saya kontak dengan penerbit teknis ...
Daniel C. Sobral
2
Objek paket dari para sahabat dari bagian-bagian dari tipe juga dicari. lampsvn.epfl.ch/trac/scala/ticket/4427
retronym
1
Dalam hal ini, itu bagian dari ruang lingkup implisit. Situs panggilan tidak harus berada dalam paket itu. Itu mengejutkan saya.
retronym
2
Ya, jadi stackoverflow.com/questions/8623055 mencakup hal itu secara khusus, tetapi saya perhatikan Anda menulis "Daftar berikut ini dimaksudkan untuk disajikan dalam urutan prioritas ... tolong laporkan." Pada dasarnya, daftar bagian dalam harus tidak teratur karena semuanya memiliki bobot yang sama (setidaknya dalam 2.10).
Eugene Yokota
23

Saya ingin mengetahui prioritas resolusi parameter implisit, bukan hanya di mana terlihat, jadi saya menulis posting blog meninjau kembali implisit tanpa pajak impor (dan parameter implisit diutamakan lagi setelah beberapa umpan balik).

Berikut daftarnya:

  • 1) tersirat terlihat oleh ruang lingkup permintaan saat ini melalui deklarasi lokal, impor, lingkup luar, warisan, objek paket yang dapat diakses tanpa awalan.
  • 2) ruang lingkup implisit , yang berisi semua jenis objek pengiring dan objek paket yang mengandung beberapa hubungan dengan tipe implisit yang kita cari (yaitu objek paket dari tipe, objek pengiring dari tipe itu sendiri, dari konstruktor tipenya jika ada, dari parameternya jika ada, dan juga dari supertype dan supertraitsnya).

Jika pada setiap tahap kita menemukan lebih dari satu implisit, aturan overloading statis digunakan untuk menyelesaikannya.

Eugene Yokota
sumber
3
Ini dapat ditingkatkan jika Anda menulis beberapa kode hanya mendefinisikan paket, objek, sifat dan kelas, dan menggunakan huruf mereka ketika Anda merujuk ke ruang lingkup. Tidak perlu menempatkan deklarasi metode apa pun - hanya nama dan siapa yang memperpanjang siapa, dan dalam lingkup mana.
Daniel C. Sobral