Scala tìm kiếm những ẩn ý ở đâu?


398

Một câu hỏi ngầm cho những người mới đến Scala dường như là: trình biên dịch tìm kiếm ẩn ý ở đâu? Ý tôi là ngầm vì câu hỏi dường như không bao giờ được hình thành đầy đủ, như thể không có từ nào cho nó. :-) Ví dụ: các giá trị integralbên dưới đến từ đâu?

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

Một câu hỏi khác liên quan đến những người quyết định tìm hiểu câu trả lời cho câu hỏi đầu tiên là làm thế nào trình biên dịch chọn sử dụng ẩn nào, trong một số tình huống mơ hồ rõ ràng (nhưng dù sao cũng sẽ biên dịch)?

Chẳng hạn, scala.Predefxác định hai chuyển đổi từ String: một sang WrappedStringmột khác StringOps. Cả hai lớp, tuy nhiên, chia sẻ rất nhiều phương thức, vậy tại sao Scala không phàn nàn về sự mơ hồ khi, nói, gọi map?

Lưu ý: câu hỏi này được lấy cảm hứng từ câu hỏi khác này , với hy vọng nêu vấn đề một cách tổng quát hơn. Ví dụ đã được sao chép từ đó, bởi vì nó được đề cập trong câu trả lời.

Câu trả lời:


554

Các loại hình ảnh

Ý nghĩa trong Scala đề cập đến một giá trị có thể được chuyển "tự động", có thể nói, hoặc chuyển đổi từ loại này sang loại khác được thực hiện tự động.

Chuyển đổi ngầm định

Nói rất ngắn gọn về loại sau, nếu người ta gọi một phương thức mtrên một đối tượng ocủa một lớp Cvà lớp đó không hỗ trợ phương thức m, thì Scala sẽ tìm kiếm một chuyển đổi ngầm định từ Cmột thứ hỗ trợ m. Một ví dụ đơn giản sẽ là phương pháp maptrên String:

"abc".map(_.toInt)

Stringkhông hỗ trợ phương pháp này map, nhưng StringOpskhông, và có một chuyển đổi ngầm từ Stringđể StringOpssẵn (xem implicit def augmentStringtrên Predef).

Thông số tiềm ẩn

Các loại ẩn khác là tham số ngầm . Chúng được truyền cho các cuộc gọi phương thức như bất kỳ tham số nào khác, nhưng trình biên dịch cố gắng tự động điền chúng vào. Nếu không thể, nó sẽ phàn nàn. Người ta có thể vượt qua các tham số này một cách rõ ràng, đó là cách người ta sử dụng breakOut, ví dụ (xem câu hỏi về breakOut, vào một ngày bạn cảm thấy khó khăn cho một thách thức).

Trong trường hợp này, người ta phải khai báo sự cần thiết của một ẩn, chẳng hạn như fookhai báo phương thức:

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

Xem giới hạn

Có một tình huống trong đó một ẩn là cả chuyển đổi ngầm và tham số ngầm. Ví dụ:

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

getIndex("abc", 'a')

Phương thức getIndexcó thể nhận bất kỳ đối tượng nào, miễn là có một chuyển đổi ngầm định có sẵn từ lớp của nó sang Seq[T]. Vì lý do đó, tôi có thể vượt qua một Stringđến getIndex, và nó sẽ làm việc.

Đằng sau hậu trường, trình biên dịch thay đổi seq.IndexOf(value)thành conv(seq).indexOf(value).

Điều này hữu ích đến nỗi có đường cú pháp để viết chúng. Sử dụng đường cú pháp này, getIndexcó thể được định nghĩa như thế này:

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

Đường cú pháp này được mô tả như là một khung nhìn bị ràng buộc , gần giống với giới hạn trên ( CC <: Seq[Int]) hoặc giới hạn dưới ( T >: Null).

Giới hạn bối cảnh

Một mẫu phổ biến khác trong các tham số ngầm là mẫu lớp . Mẫu này cho phép cung cấp các giao diện chung cho các lớp không khai báo chúng. Nó vừa có thể phục vụ như một mẫu cầu nối - đạt được sự phân tách các mối quan tâm - vừa là một mẫu bộ điều hợp.

Các Integrallớp học mà bạn đề cập là một ví dụ điển hình của kiểu dữ liệu mẫu lớp. Một ví dụ khác về thư viện tiêu chuẩn của Scala là Ordering. Có một thư viện sử dụng rất nhiều mẫu này, được gọi là Scalaz.

Đây là một ví dụ về việc sử dụng nó:

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

Ngoài ra còn có cú pháp cú pháp cho nó, được gọi là bối cảnh ràng buộc , được làm cho ít hữu ích hơn bởi sự cần thiết phải đề cập đến ẩn. Một chuyển đổi thẳng của phương thức đó trông như thế này:

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)(_ + _)
}

Giới hạn bối cảnh hữu ích hơn khi bạn chỉ cần chuyển chúng sang các phương thức khác sử dụng chúng. Ví dụ, phương pháp sortedtrên Seqcần một ẩn Ordering. Để tạo một phương thức reverseSort, người ta có thể viết:

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

Bởi vì Ordering[T]đã được truyền vào ngầm reverseSort, sau đó nó có thể chuyển nó hoàn toàn sang sorted.

Implicits đến từ đâu?

Khi trình biên dịch thấy sự cần thiết của một ẩn, vì bạn đang gọi một phương thức không tồn tại trên lớp của đối tượng hoặc bởi vì bạn đang gọi một phương thức yêu cầu một tham số ẩn, nó sẽ tìm kiếm một ẩn sẽ phù hợp với nhu cầu .

Tìm kiếm này tuân theo các quy tắc nhất định xác định những ẩn ý nào có thể nhìn thấy và không. Bảng dưới đây cho thấy trình biên dịch sẽ tìm kiếm ẩn ý được lấy từ một bài thuyết trình tuyệt vời về ẩn ý của Josh Suereth, mà tôi chân thành giới thiệu cho bất kỳ ai muốn cải thiện kiến ​​thức Scala của họ. Nó đã được bổ sung kể từ đó với thông tin phản hồi và cập nhật.

Các ẩn ý có sẵn dưới số 1 dưới đây được ưu tiên so với các ẩn dưới số 2. Khác với điều đó, nếu có một số đối số đủ điều kiện khớp với loại tham số ẩn, một đối số cụ thể nhất sẽ được chọn bằng cách sử dụng quy tắc phân giải quá tải tĩnh (xem Scala Đặc điểm kỹ thuật §6.26.3). Thông tin chi tiết hơn có thể được tìm thấy trong một câu hỏi tôi liên kết đến cuối câu trả lời này.

  1. Cái nhìn đầu tiên trong phạm vi hiện tại
    • Ý nghĩa được xác định trong phạm vi hiện tại
    • Nhập khẩu rõ ràng
    • nhập khẩu ký tự đại diện
    • Cùng phạm vi trong các tệp khác
  2. Bây giờ hãy nhìn vào các loại liên quan trong
    • Đối tượng đồng hành của một loại
    • Phạm vi ngầm định của loại đối số (2.9.1)
    • Phạm vi ngầm định của các đối số kiểu (2.8.0)
    • Các đối tượng bên ngoài cho các loại lồng nhau
    • Kích thước khác

Hãy cho một số ví dụ cho họ:

Ý nghĩa được xác định trong phạm vi hiện tại

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

Nhập khẩu rõ ràng

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

Nhập khẩu ký tự đại diện

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)(_ + _)
}

Cùng phạm vi trong các tệp khác

Chỉnh sửa : Có vẻ như điều này không có một ưu tiên khác. Nếu bạn có một số ví dụ chứng minh sự phân biệt ưu tiên, vui lòng đưa ra nhận xét. Nếu không, đừng dựa vào cái này.

Đây giống như ví dụ đầu tiên, nhưng giả sử định nghĩa ngầm định nằm trong một tệp khác với cách sử dụng. Xem thêm cách các đối tượng gói có thể được sử dụng để mang lại ẩn ý.

Đối tượng đồng hành của một loại

Có hai đối tượng lưu ý ở đây. Đầu tiên, đối tượng đồng hành của loại "nguồn" được xem xét. Chẳng hạn, bên trong đối tượng Optioncó một chuyển đổi ngầm định Iterable, vì vậy người ta có thể gọi Iterablecác phương thức trên Optionhoặc chuyển Optionđến một cái gì đó mong đợi một Iterable. Ví dụ:

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

Biểu thức đó được dịch bởi trình biên dịch sang

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

Tuy nhiên, List.flatMapmong đợi một TraversableOnce, đó Optionlà không. Trình biên dịch sau đó trông bên trong Optioncủa bạn đồng hành đối tượng và tìm thấy việc chuyển đổi sang Iterable, mà là một TraversableOnce, làm cho biểu hiện này chính xác.

Thứ hai, đối tượng đồng hành của loại dự kiến:

List(1, 2, 3).sorted

Phương pháp sortednày có một ẩn Ordering. Trong trường hợp này, nó nhìn vào bên trong đối tượng Ordering, đồng hành với lớp Orderingvà tìm thấy một ẩn Ordering[Int]ở đó.

Lưu ý rằng các đối tượng đồng hành của các siêu lớp cũng được xem xét. Ví dụ:

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"

Đây là cách Scala tìm thấy ẩn ý Numeric[Int]Numeric[Long]trong câu hỏi của bạn, nhân tiện, như chúng được tìm thấy bên trong Numeric, không phải Integral.

Phạm vi tiềm ẩn của loại đối số

Nếu bạn có một phương thức với một kiểu đối số A, thì phạm vi ngầm định của kiểu Acũng sẽ được xem xét. Theo "phạm vi ngầm", ý tôi là tất cả các quy tắc này sẽ được áp dụng đệ quy - ví dụ: đối tượng đồng hành của Asẽ được tìm kiếm cho các ẩn ý, ​​theo quy tắc trên.

Lưu ý rằng điều này không có nghĩa là phạm vi ngầm định Asẽ được tìm kiếm để chuyển đổi tham số đó, mà là toàn bộ biểu thức. Ví dụ:

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)

Điều này có sẵn kể từ Scala 2.9.1.

Phạm vi tiềm ẩn của các đối số loại

Điều này là cần thiết để làm cho mẫu lớp loại thực sự hoạt động. OrderingVí dụ, hãy xem xét : Nó đi kèm với một số ẩn ý trong đối tượng đồng hành của nó, nhưng bạn không thể thêm nội dung vào nó. Vì vậy, làm thế nào bạn có thể tạo một Orderinglớp cho riêng bạn được tìm thấy tự động?

Hãy bắt đầu với việc thực hiện:

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

Vì vậy, hãy xem xét những gì xảy ra khi bạn gọi

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

Như chúng ta đã thấy, phương thức sortedmong đợi một Ordering[A](thực sự, nó mong đợi một Ordering[B], ở đâu B >: A). Không có bất kỳ thứ gì như vậy bên trong Ordering, và không có loại "nguồn" nào để tìm. Rõ ràng, nó được tìm thấy nó bên trong A, mà là một đối số kiểu của Ordering.

Đây cũng là cách các phương thức thu thập khác nhau mong đợi CanBuildFromhoạt động: các ẩn ý được tìm thấy bên trong các đối tượng đồng hành với các tham số loại của CanBuildFrom.

Lưu ý : Orderingđược định nghĩa là trait Ordering[T], trong đó Tlà một tham số loại. Trước đây, tôi đã nói rằng Scala nhìn vào bên trong các tham số loại, điều này không có ý nghĩa nhiều. Các tiềm ẩn đã tìm kiếm ở trên là Ordering[A], nơi Alà một loại thực tế, không phải gõ tham số: nó là một đối số kiểu để Ordering. Xem phần 7.2 của đặc tả Scala.

Điều này có sẵn kể từ Scala 2.8.0.

Đối tượng bên ngoài cho các loại lồng nhau

Tôi chưa thực sự thấy các ví dụ về điều này. Tôi sẽ biết ơn nếu ai đó có thể chia sẻ một. Nguyên tắc rất đơn giản:

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"

Kích thước khác

Tôi khá chắc chắn đây là một trò đùa, nhưng câu trả lời này có thể không cập nhật. Vì vậy, đừng coi câu hỏi này là trọng tài cuối cùng của những gì đang xảy ra, và nếu bạn nhận thấy nó đã lỗi thời, vui lòng thông báo cho tôi để tôi có thể khắc phục nó.

BIÊN TẬP

Các câu hỏi liên quan:


60
Đã đến lúc bạn bắt đầu sử dụng câu trả lời của mình trong một cuốn sách, bây giờ chỉ còn là vấn đề của việc kết hợp tất cả lại với nhau.
pedrofurla

3
@pedrofurla Tôi đã được cân nhắc viết một cuốn sách bằng tiếng Bồ Đào Nha. Nếu ai đó có thể tìm cho tôi một liên hệ với nhà xuất bản kỹ thuật ...
Daniel C. Sobral

2
Các đối tượng gói của các bạn đồng hành của các phần của loại cũng được tìm kiếm. đènvn.epfl.ch/trac/scala/ticket/4427
retronym

1
Trong trường hợp này, nó là một phần của phạm vi ngầm. Các trang web cuộc gọi không cần phải nằm trong gói đó. Đó là điều đáng ngạc nhiên đối với tôi.
retronym

2
Phải, vì vậy stackoverflow.com/questions/8623055 bao gồm điều đó một cách cụ thể, nhưng tôi nhận thấy bạn đã viết "Danh sách sau đây được dự định sẽ được trình bày theo thứ tự ưu tiên ... vui lòng báo cáo." Về cơ bản, các danh sách bên trong nên được sắp xếp theo thứ tự vì tất cả chúng đều có trọng lượng bằng nhau (ít nhất là trong 2,10).
Eugene Yokota

23

Tôi muốn tìm hiểu mức độ ưu tiên của độ phân giải tham số ngầm, không chỉ là nơi nó tìm kiếm, vì vậy tôi đã viết một bài đăng trên blog xem lại các ẩn ý mà không có thuế nhập khẩu (và ưu tiên tham số ẩn sau một số phản hồi).

Đây là danh sách:

  • 1) ngụ ý phạm vi gọi hiện tại thông qua khai báo cục bộ, nhập, phạm vi bên ngoài, kế thừa, đối tượng gói có thể truy cập mà không có tiền tố.
  • 2) phạm vi ẩn , chứa tất cả các loại đối tượng đồng hành và đối tượng gói có liên quan đến loại ẩn mà chúng ta tìm kiếm (tức là đối tượng gói của loại, đối tượng đồng hành của chính loại đó, của hàm tạo kiểu của nó nếu có, của thông số của nó nếu có, và cả siêu siêu và siêu phẳng của nó).

Nếu ở một trong hai giai đoạn chúng ta tìm thấy nhiều hơn một quy tắc ngầm, quá tải tĩnh được sử dụng để giải quyết nó.


3
Điều này có thể được cải thiện nếu bạn đã viết một số mã chỉ xác định các gói, đối tượng, đặc điểm và lớp và sử dụng các chữ cái của chúng khi bạn tham chiếu phạm vi. Không cần phải đặt bất kỳ khai báo phương thức nào - chỉ cần tên và ai mở rộng ai, và trong phạm vi nào.
Daniel C. Sobral
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.