Chuyển đổi ngầm so với loại loại


93

Trong Scala, chúng tôi có thể sử dụng ít nhất hai phương pháp để trang bị thêm các loại hiện có hoặc mới. Giả sử chúng ta muốn thể hiện rằng một cái gì đó có thể được định lượng bằng cách sử dụng một Int. Chúng ta có thể xác định đặc điểm sau.

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

trait Quantifiable{ def quantify: Int }

Và sau đó chúng ta có thể sử dụng các chuyển đổi ngầm định để định lượng, ví dụ: Chuỗi và Danh sách.

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

Sau khi nhập chúng, chúng ta có thể gọi phương thức quantifytrên chuỗi và danh sách. Lưu ý rằng danh sách có thể định lượng lưu trữ độ dài của nó, do đó, nó tránh việc duyệt danh sách tốn kém trong các lần gọi tiếp theo quantify.

Loại lớp

Một giải pháp thay thế là xác định một "nhân chứng" Quantified[A]nói rằng, một số loại Acó thể được định lượng.

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

Sau đó, chúng tôi cung cấp các thể hiện của lớp loại này cho StringListở đâu đó.

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

Và nếu sau đó chúng ta viết một phương thức cần định lượng các đối số của nó, chúng ta sẽ viết:

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

Hoặc sử dụng cú pháp liên kết ngữ cảnh:

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

Nhưng khi nào thì sử dụng phương pháp nào?

Bây giờ đến câu hỏi. Làm thế nào tôi có thể quyết định giữa hai khái niệm đó?

Những gì tôi đã nhận thấy cho đến nay.

loại lớp

  • các lớp kiểu cho phép cú pháp ràng buộc ngữ cảnh đẹp
  • với các lớp kiểu, tôi không tạo đối tượng trình bao bọc mới mỗi lần sử dụng
  • cú pháp ràng buộc ngữ cảnh không hoạt động nữa nếu lớp kiểu có nhiều tham số kiểu; Hãy tưởng tượng tôi muốn định lượng mọi thứ không chỉ với số nguyên mà với các giá trị của một số loại tổng quát T. Tôi muốn tạo một loại lớpQuantified[A,T]

chuyển đổi ngầm

  • vì tôi tạo một đối tượng mới, tôi có thể lưu các giá trị vào bộ nhớ cache ở đó hoặc tính toán một biểu diễn tốt hơn; nhưng tôi có nên tránh điều này không, vì nó có thể xảy ra vài lần và một chuyển đổi rõ ràng có thể chỉ được gọi một lần?

Những gì tôi mong đợi từ một câu trả lời

Trình bày một (hoặc nhiều) trường hợp sử dụng mà sự khác biệt giữa cả hai khái niệm đều quan trọng và giải thích lý do tại sao tôi thích cái này hơn cái kia. Ngoài ra, giải thích bản chất của hai khái niệm và mối quan hệ của chúng với nhau sẽ rất tốt, ngay cả khi không có ví dụ.


Có một số nhầm lẫn trong các điểm lớp kiểu mà bạn đề cập đến "giới hạn chế độ xem", mặc dù các lớp kiểu sử dụng giới hạn ngữ cảnh.
Daniel C. Sobral

1
+1 câu hỏi xuất sắc; Tôi rất quan tâm đến một câu trả lời thấu đáo cho điều này.
Dan Burton

@Daniel Cảm ơn bạn. Tôi luôn luôn nhận sai.
ziggystar

2
Bạn đang nhầm lẫn ở một chỗ: trong ví dụ chuyển đổi ngầm thứ hai, bạn lưu trữ sizedanh sách trong một giá trị và nói rằng nó tránh việc chuyển danh sách tốn kém trong các lần gọi tiếp theo để định lượng, nhưng mỗi lần gọi đến giá quantifytrị list2quantifiableđều được kích hoạt tất cả lại một lần nữa do đó phục hồi Quantifiablevà tính toán lại quantifytài sản. Những gì tôi đang nói là thực sự không có cách nào để lưu kết quả vào bộ nhớ cache với các chuyển đổi ngầm.
Nikita Volkov

@NikitaVolkov Quan sát của bạn là đúng. Và tôi nhấn mạnh điều này trong câu hỏi của tôi trong đoạn thứ hai đến cuối cùng. Bộ nhớ đệm hoạt động khi đối tượng được chuyển đổi được sử dụng lâu hơn sau một lần gọi phương thức chuyển đổi (và có thể được chuyển sang dạng đã chuyển đổi của nó). Trong khi các lớp kiểu có thể sẽ bị xâu chuỗi dọc theo đối tượng chưa được chuyển đổi khi đi sâu hơn.
ziggystar

Câu trả lời:


42

Mặc dù tôi không muốn sao chép tài liệu của mình từ Scala In Depth , nhưng tôi nghĩ cần lưu ý rằng các lớp kiểu / đặc điểm kiểu linh hoạt hơn vô cùng.

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

có khả năng tìm kiếm môi trường cục bộ của nó cho một lớp kiểu mặc định. Tuy nhiên, tôi có thể ghi đè hành vi mặc định bất kỳ lúc nào bằng một trong hai cách:

  1. Tạo / nhập một phiên bản lớp kiểu ngầm định trong Phạm vi tìm kiếm ngầm hiểu ngắn mạch
  2. Trực tiếp chuyển một lớp loại

Đây là một ví dụ:

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
}

Điều này làm cho các lớp kiểu linh hoạt hơn vô hạn. Một điều nữa là các lớp / đặc điểm kiểu hỗ trợ tra cứu ngầm tốt hơn.

Trong ví dụ đầu tiên của bạn, nếu bạn sử dụng chế độ xem ngầm định, trình biên dịch sẽ thực hiện tra cứu ngầm định đối với:

Function1[Int, ?]

Mà sẽ xem xét Function1đối tượng đồng hành của và Intđối tượng đồng hành.

Chú ý rằng Quantifiablenơi nào trong tra cứu tiềm ẩn. Điều này có nghĩa là bạn phải đặt chế độ xem ngầm trong một đối tượng gói hoặc nhập nó vào phạm vi. Việc ghi nhớ những gì đang diễn ra còn nhiều việc hơn.

Mặt khác, một lớp kiểu là rõ ràng . Bạn thấy những gì nó đang tìm kiếm trong chữ ký phương thức. Bạn cũng có một tra cứu ngầm về

Quantifiable[Int]

sẽ tìm trong Quantifiableđối tượng đồng hành của Int đối tượng đồng hành của. Có nghĩa là bạn có thể cung cấp giá trị mặc định các kiểu mới (như một MyStringlớp) có thể cung cấp giá trị mặc định trong đối tượng đồng hành của chúng và nó sẽ được tìm kiếm ngầm.

Nói chung, tôi sử dụng các lớp kiểu. Chúng linh hoạt hơn vô hạn đối với ví dụ ban đầu. Nơi duy nhất tôi sử dụng chuyển đổi ngầm là khi sử dụng lớp API giữa trình bao bọc Scala và thư viện Java, và thậm chí điều này có thể 'nguy hiểm' nếu bạn không cẩn thận.


20

Một tiêu chí có thể phát huy tác dụng là bạn muốn tính năng mới có "cảm giác" như thế nào; bằng cách sử dụng chuyển đổi ngầm định, bạn có thể làm cho nó giống như một phương pháp khác:

"my string".newFeature

... trong khi sử dụng các lớp kiểu, nó sẽ luôn trông giống như bạn đang gọi một hàm bên ngoài:

newFeature("my string")

Một điều mà bạn có thể đạt được với các lớp kiểu chứ không phải với các chuyển đổi ngầm định là thêm thuộc tính vào một kiểu , thay vì vào một thể hiện của kiểu. Sau đó, bạn có thể truy cập các thuộc tính này ngay cả khi bạn không có sẵn phiên bản của loại. Một ví dụ chuẩn sẽ là:

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

Ví dụ này cũng cho thấy các khái niệm có liên quan chặt chẽ với nhau như thế nào: các lớp kiểu sẽ gần như không hữu ích nếu không có cơ chế tạo ra vô số trường hợp của chúng; nếu không có implicitphương thức (không phải là chuyển đổi, phải thừa nhận), tôi chỉ có thể có rất nhiều loại có thuộc Defaulttính.


@Phillippe - Tôi rất quan tâm đến kỹ thuật bạn đã viết ... nhưng nó có vẻ không hoạt động trên Scala 2.11.6. Tôi đã đăng một câu hỏi yêu cầu cập nhật câu trả lời của bạn. cảm ơn trước nếu bạn có thể giúp: Vui lòng xem: stackoverflow.com/questions/31910923/…
Chris Bedford

@ChrisBedford Tôi đã thêm định nghĩa defaultcho những người đọc trong tương lai.
Philippe

13

Bạn có thể nghĩ về sự khác biệt giữa hai kỹ thuật bằng cách tương tự với ứng dụng chức năng, chỉ với một trình bao bọc được đặt tên. Ví dụ:

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

Một thể hiện của cái trước đóng gói một hàm kiểu A => Int, trong khi một thể hiện của cái sau đã được áp dụng cho một A. Bạn có thể tiếp tục mô hình ...

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

do đó bạn có thể nghĩ Foo1[B]giống như ứng dụng một phần của Foo2[A, B]một số Atrường hợp. Một ví dụ tuyệt vời về điều này đã được Miles Sabin viết ra là "Sự phụ thuộc chức năng trong Scala" .

Vì vậy, thực sự quan điểm của tôi là, về nguyên tắc:

  • "pimping" một lớp (thông qua chuyển đổi ngầm định) là trường hợp "thứ tự thứ không" ...
  • khai báo một typeclass là trường hợp "đơn hàng đầu tiên" ...
  • các kiểu chữ đa thông số với mã khóa (hoặc một cái gì đó như mã khóa) là trường hợp chung.
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.