Một số trường hợp sử dụng hấp dẫn cho các loại phương pháp phụ thuộc là gì?


127

Các loại phương thức phụ thuộc, trước đây là một tính năng thử nghiệm, hiện đã được bật theo mặc định trong thân cây và dường như điều này dường như đã tạo ra sự phấn khích trong cộng đồng Scala.

Thoạt nhìn, không rõ ràng điều này có thể hữu ích cho việc gì. Heiko Seeberger đã đăng một ví dụ đơn giản về các loại phương thức phụ thuộc ở đây , có thể thấy trong bình luận có thể dễ dàng được sao chép với các tham số loại trên các phương thức. Vì vậy, đó không phải là một ví dụ rất hấp dẫn. (Tôi có thể thiếu một cái gì đó rõ ràng. Xin vui lòng sửa cho tôi nếu có.)

Một số ví dụ thực tế và hữu ích của các trường hợp sử dụng cho các loại phương thức phụ thuộc nơi chúng rõ ràng là lợi thế so với các lựa chọn thay thế?

Những điều thú vị nào chúng ta có thể làm với chúng mà trước đây không thể / dễ dàng?

Họ mua gì cho chúng tôi qua các tính năng hệ thống hiện có?

Ngoài ra, các loại phương thức phụ thuộc có giống với hoặc lấy cảm hứng từ bất kỳ tính năng nào được tìm thấy trong các hệ thống loại của các ngôn ngữ được nhập nâng cao khác như Haskell, OCaml không?


Bạn có thể quan tâm đến việc truy cập haskell.org/haskellwiki/Deperee_type
Dan Burton

Cảm ơn các liên kết, Dan! Tôi nhận thức được các loại phụ thuộc nói chung, nhưng khái niệm về các loại phương thức phụ thuộc là tương đối mới đối với tôi.
missingfaktor

Dường như với tôi rằng "các loại phương thức phụ thuộc" chỉ đơn giản là các loại phụ thuộc vào một hoặc nhiều loại đầu vào của phương thức (bao gồm cả loại đối tượng mà phương thức được gọi); Không có gì điên rồ ngoài ý tưởng chung về các loại phụ thuộc. Có lẽ tôi đang thiếu một cái gì đó?
Dan Burton

Không, bạn đã không, nhưng rõ ràng tôi đã làm. :-) Tôi không thấy liên kết giữa hai cái trước. Bây giờ thì rõ ràng rồi.
năm11

Câu trả lời:


112

Nhiều hơn hoặc ít hơn bất kỳ việc sử dụng các loại thành viên (tức là lồng nhau) có thể làm phát sinh nhu cầu về các loại phương thức phụ thuộc. Cụ thể, tôi duy trì rằng không có các kiểu phương thức phụ thuộc, kiểu bánh cổ điển gần giống với kiểu chống mẫu hơn.

Vậy vấn đề là gì? Các kiểu lồng nhau trong Scala phụ thuộc vào thể hiện kèm theo của chúng. Do đó, trong trường hợp không có các loại phương thức phụ thuộc, các nỗ lực sử dụng chúng bên ngoài trường hợp đó có thể gây khó khăn một cách khó chịu. Điều này có thể biến những thiết kế ban đầu có vẻ thanh lịch và lôi cuốn thành những thứ quái dị, cứng nhắc và khó tái cấu trúc.

Tôi sẽ minh họa rằng với một bài tập tôi đưa ra trong khóa đào tạo Advanced Scala của mình ,

trait ResourceManager {
  type Resource <: BasicResource
  trait BasicResource {
    def hash : String
    def duplicates(r : Resource) : Boolean
  }
  def create : Resource

  // Test methods: exercise is to move them outside ResourceManager
  def testHash(r : Resource) = assert(r.hash == "9e47088d")  
  def testDuplicates(r : Resource) = assert(r.duplicates(r))
}

trait FileManager extends ResourceManager {
  type Resource <: File
  trait File extends BasicResource {
    def local : Boolean
  }
  override def create : Resource
}

class NetworkFileManager extends FileManager {
  type Resource = RemoteFile
  class RemoteFile extends File {
    def local = false
    def hash = "9e47088d"
    def duplicates(r : Resource) = (local == r.local) && (hash == r.hash)
  }
  override def create : Resource = new RemoteFile
}

Đó là một ví dụ về mô hình bánh cổ điển: chúng ta có một gia đình trừu tượng dần dần được tinh chế thông qua chế độ bá đạo ( ResourceManager/ Resourceđược tinh chế bởi FileManager/ Filelần lượt được tinh chỉnh bởi NetworkFileManager/RemoteFile ). Đây là một ví dụ về đồ chơi, nhưng mô hình là có thật: nó được sử dụng trong suốt trình biên dịch Scala và được sử dụng rộng rãi trong plugin Scala Eclipse.

Đây là một ví dụ về sự trừu tượng trong sử dụng,

val nfm = new NetworkFileManager
val rf : nfm.Resource = nfm.create
nfm.testHash(rf)
nfm.testDuplicates(rf)

Lưu ý rằng phụ thuộc đường dẫn có nghĩa là trình biên dịch sẽ đảm bảo rằng các phương thức testHashvà chỉ có thể được gọi với các đối số tương ứng với nó, tức là. nó là của riêng bạn , và không có gì khác.testDuplicatesNetworkFileManagerRemoteFiles

Không thể phủ nhận đó là một thuộc tính mong muốn, nhưng giả sử chúng tôi muốn chuyển mã kiểm tra này sang một tệp nguồn khác? Với các loại phương thức phụ thuộc, thật dễ dàng để xác định lại các phương thức đó bên ngoài ResourceManagerhệ thống phân cấp,

def testHash4(rm : ResourceManager)(r : rm.Resource) = 
  assert(r.hash == "9e47088d")

def testDuplicates4(rm : ResourceManager)(r : rm.Resource) = 
  assert(r.duplicates(r))

Lưu ý việc sử dụng các loại phương thức phụ thuộc ở đây: loại đối số thứ hai ( rm.Resource) phụ thuộc vào giá trị của đối số thứ nhất ( rm).

Có thể thực hiện việc này mà không cần các loại phương pháp phụ thuộc, nhưng nó cực kỳ lúng túng và cơ chế này khá không trực quan: Tôi đã dạy khóa học này gần hai năm nay và trong thời gian đó, không có ai đưa ra giải pháp làm việc chưa được thực hiện.

Hãy thử nó cho chính mình ...

// Reimplement the testHash and testDuplicates methods outside
// the ResourceManager hierarchy without using dependent method types
def testHash        // TODO ... 
def testDuplicates  // TODO ...

testHash(rf)
testDuplicates(rf)

Sau một thời gian ngắn vật lộn với nó, có lẽ bạn sẽ khám phá ra lý do tại sao tôi (hoặc có thể là David MacIver, chúng tôi không thể nhớ ai trong số chúng tôi đặt ra thuật ngữ này) gọi đây là Bakery of Doom.

Chỉnh sửa: sự đồng thuận là Bakery of Doom là đồng tiền của David MacIver ...

Về phần thưởng: Hình thức các loại phụ thuộc của Scala nói chung (và các loại phương thức phụ thuộc như một phần của nó) được lấy cảm hứng từ ngôn ngữ lập trình Beta ... chúng phát sinh một cách tự nhiên từ ngữ nghĩa lồng nhau nhất quán của Beta. Tôi không biết bất kỳ ngôn ngữ lập trình chính thống thậm chí mờ nhạt nào khác có các loại phụ thuộc trong hình thức này. Các ngôn ngữ như Coq, Cayenne, Epigram và Agda có một kiểu gõ phụ thuộc khác, theo một cách nào đó chung chung hơn, nhưng khác biệt đáng kể bởi là một phần của các hệ thống loại, không giống như Scala, không có phân nhóm.


2
Chính David MacIver đã đặt ra thuật ngữ này, nhưng trong mọi trường hợp, nó khá mô tả. Đây là một lời giải thích tuyệt vời về lý do tại sao các loại phương thức phụ thuộc rất thú vị. Công việc tốt đẹp!
Daniel Spiewak

Nó xuất hiện ban đầu trong cuộc trò chuyện giữa hai chúng tôi trên #scala cách đây khá lâu ... như tôi đã nói tôi không thể nhớ ai trong số chúng tôi đã nói điều đó trước.
Miles Sabin

Có vẻ như trí nhớ của tôi đã giở trò đồi bại với tôi ... sự đồng thuận là đó là đồng tiền của David MacIver.
Miles Sabin

Vâng, tôi đã không ở đó (trên #scala) vào thời điểm đó, nhưng Jorge đã ở đó và đó là nơi tôi đang lấy thông tin của mình.
Daniel Spiewak

Bằng cách sử dụng sàng lọc thành viên loại trừu tượng tôi đã có thể thực hiện chức năng testHash4 khá dễ dàng. def testHash4[R <: ResourceManager#BasicResource](rm: ResourceManager { type Resource = R }, r: R) = assert(r.hash == "9e47088d")Tôi cho rằng điều này có thể được coi là một dạng khác của các loại phụ thuộc, mặc dù.
Marco van Hilst

53
trait Graph {
  type Node
  type Edge
  def end1(e: Edge): Node
  def end2(e: Edge): Node
  def nodes: Set[Node]
  def edges: Set[Edge]
}

Ở một nơi khác, chúng tôi có thể đảm bảo tĩnh rằng chúng tôi không trộn lẫn các nút từ hai biểu đồ khác nhau, ví dụ:

def shortestPath(g: Graph)(n1: g.Node, n2: g.Node) = ... 

Tất nhiên, điều này đã hoạt động nếu được xác định bên trong Graph, nhưng nói rằng chúng tôi không thể sửa đổi Graphvà đang viết phần mở rộng "pimp my library" cho nó.

Về câu hỏi thứ hai: Các loại kích hoạt tính năng này là xa yếu hơn so với các loại phụ thuộc đầy đủ (Xem lệ thuộc Typed trình trong Agda Tôi không nghĩ rằng tôi đã nhìn thấy một tương tự trước khi cho một hương vị đó.).


6

Tính năng mới này là cần thiết khi các thành viên loại trừu tượng cụ thể được sử dụng thay vì tham số loại . Khi các tham số loại được sử dụng, sự phụ thuộc loại đa hình gia đình có thể được thể hiện trong phiên bản mới nhất và một số phiên bản cũ hơn của Scala, như trong ví dụ đơn giản sau đây.

trait C[A]
def f[M](a: C[M], b: M) = b
class C1 extends C[Int]
class C2 extends C[String]

f(new C1, 0)
res0: Int = 0
f(new C2, "")
res1: java.lang.String = 
f(new C1, "")
error: type mismatch;
 found   : C1
 required: C[Any]
       f(new C1, "")
         ^

Điều này không liên quan. Với các thành viên loại, bạn có thể sử dụng các sàng lọc cho cùng một kết quả: trait C {type A}; def f[M](a: C { type A = M}, b: M) = 0;class CI extends C{type A=Int};class CS extends C{type A=String}vv
nafg

Trong mọi trường hợp, điều này không liên quan gì đến các kiểu phương thức phụ thuộc. Lấy ví dụ về ví dụ của Alexey ( stackoverflow.com/a/7860821/333643 ). Sử dụng phương pháp của bạn (bao gồm cả phiên bản sàng lọc mà tôi đã nhận xét) không đạt được mục tiêu. Nó sẽ đảm bảo rằng n1.Node =: = n2.Node, nhưng nó sẽ không đảm bảo cả hai đều nằm trong cùng một biểu đồ. IIUC DMT không đảm bảo điều này.
nafg

@nafg Cảm ơn bạn đã chỉ ra điều đó. Tôi đã thêm từ cụ thể để làm rõ rằng tôi không đề cập đến trường hợp sàng lọc cho các thành viên loại. Theo như tôi có thể thấy, đây vẫn là trường hợp sử dụng hợp lệ cho các loại phương thức phụ thuộc mặc dù quan điểm của bạn (mà tôi biết) rằng chúng có thể có nhiều quyền lực hơn trong các trường hợp sử dụng khác. Hay tôi đã bỏ lỡ bản chất cơ bản của bình luận thứ hai của bạn?
Shelby Moore III

3

Tôi đang phát triển một mô hình cho sự can thiệp của một hình thức lập trình khai báo với trạng thái môi trường. Các chi tiết không liên quan ở đây (ví dụ: chi tiết về các cuộc gọi lại và sự tương đồng về mặt khái niệm với mô hình Diễn viên kết hợp với Bộ nối tiếp).

Vấn đề liên quan là các giá trị trạng thái được lưu trữ trong bản đồ băm và được tham chiếu bởi giá trị khóa băm. Hàm nhập các đối số bất biến là các giá trị từ môi trường, có thể gọi các hàm khác như vậy và ghi trạng thái vào môi trường. Nhưng các hàm không được phép đọc các giá trị từ môi trường (vì vậy mã bên trong của hàm không phụ thuộc vào thứ tự thay đổi trạng thái và do đó vẫn được khai báo theo nghĩa đó). Làm thế nào để gõ cái này trong Scala?

Lớp môi trường phải có một phương thức quá tải để nhập hàm như vậy để gọi và nhập các khóa băm của các đối số của hàm. Do đó phương pháp này có thể gọi hàm với các giá trị cần thiết từ bản đồ băm, mà không cung cấp quyền truy cập đọc công khai vào các giá trị (do đó, theo yêu cầu, từ chối chức năng khả năng đọc các giá trị từ môi trường).

Nhưng nếu các khóa băm này là các chuỗi hoặc giá trị băm số nguyên, thì kiểu gõ tĩnh của loại phần tử bản đồ băm sẽ thuộc về Any hoặc AnyRef (mã băm bản đồ không được hiển thị bên dưới), do đó có thể xảy ra sự không khớp thời gian chạy, do đó có thể xảy ra để đặt bất kỳ loại giá trị nào trong bản đồ băm cho một khóa băm nhất định.

trait Env {
...
  def callit[A](func: Env => Any => A, arg1key: String): A
  def callit[A](func: Env => Any => Any => A, arg1key: String, arg2key: String): A
}

Mặc dù tôi đã không kiểm tra các mục sau, nhưng về lý thuyết tôi có thể nhận được các khóa băm từ tên lớp khi sử dụng thời gian chạy classOf, vì vậy khóa băm là tên lớp thay vì chuỗi (sử dụng backticks của Scala để nhúng chuỗi vào tên lớp).

trait DependentHashKey {
  type ValueType
}
trait `the hash key string` extends DependentHashKey {
  type ValueType <: SomeType
}

Vì vậy, an toàn loại tĩnh được thực hiện.

def callit[A](arg1key: DependentHashKey)(func: Env => arg1key.ValueType => A): A

Khi chúng ta cần chuyển các khóa đối số xung quanh trong một giá trị, tôi đã không kiểm tra, nhưng giả sử chúng ta có thể sử dụng Tuple, ví dụ như quá tải 2 đối số def callit[A](argkeys: Tuple[DependentHashKey,DependentHashKey])(func: Env => argkeys._0.ValueType => argkeys._1.ValueType => A): A. Chúng tôi sẽ không sử dụng một tập hợp các khóa đối số, bởi vì các loại phần tử sẽ được thay thế (không xác định tại thời gian biên dịch) trong loại bộ sưu tập.
Shelby Moore III

"kiểu gõ tĩnh của loại phần tử bản đồ băm phụ thuộc vào Any hoặc AnyRef" - Tôi không tuân theo. Khi bạn nói loại phần tử, bạn có nghĩa là loại khóa hoặc loại giá trị (tức là đối số loại thứ nhất hoặc thứ hai với HashMap)? Và tại sao nó lại bị sụt giảm?
Robin Green

@RobinGreen Loại giá trị trong bảng băm. Afair, bị thu hẹp bởi vì bạn không thể đặt nhiều loại trong một bộ sưu tập ở Scala trừ khi bạn sử dụng loại siêu phổ biến của chúng, bởi vì Scala không có loại kết hợp (phân tách). Xem câu hỏi và trả lời của tôi về việc giảm giá trong Scala.
Shelby Moore III
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.