Bất kỳ lý do nào tại sao scala không hỗ trợ rõ ràng các loại phụ thuộc?


109

Có những kiểu phụ thuộc vào đường dẫn và tôi nghĩ rằng có thể diễn đạt gần như tất cả các tính năng của các ngôn ngữ như Epigram hoặc Agda trong Scala, nhưng tôi tự hỏi tại sao Scala không hỗ trợ điều này rõ ràng hơn giống như nó rất độc đáo trong các lĩnh vực khác (giả sử , DSL)? Bất cứ điều gì tôi đang thiếu như "nó không cần thiết"?


3
Chà, các nhà thiết kế của Scala tin rằng Khối lập phương Barendregt Lambda không phải là phần cuối của Lý thuyết loại. Đó có thể là lý do hoặc không.
Jörg W Mittag

8
@ JörgWMittag Khối lập phương Lamda là gì? Một số loại thiết bị ma thuật?
Ashkan Kh. Nazary

@ ashy_32bit xem bài báo của Barendregt "Giới thiệu về Hệ thống Loại Tổng quát" tại đây: diku.dk/hjemmesider/ansatte/henglein/papers/barendregt1991.pdf
iainmcgin

Câu trả lời:


151

Bỏ qua sự tiện lợi về mặt cú pháp, sự kết hợp của các kiểu singleton, kiểu phụ thuộc vào đường dẫn và các giá trị ngầm định có nghĩa là Scala hỗ trợ tốt một cách đáng ngạc nhiên cho việc nhập phụ thuộc, như tôi đã cố gắng chứng minh trong shapeless .

Hỗ trợ nội tại của Scala cho các kiểu phụ thuộc là thông qua các kiểu phụ thuộc vào đường dẫn . Những điều này cho phép một kiểu phụ thuộc vào đường dẫn của bộ chọn thông qua một đồ thị đối tượng- (tức là. Value-) như vậy,

scala> class Foo { class Bar }
defined class Foo

scala> val foo1 = new Foo
foo1: Foo = Foo@24bc0658

scala> val foo2 = new Foo
foo2: Foo = Foo@6f7f757

scala> implicitly[foo1.Bar =:= foo1.Bar] // OK: equal types
res0: =:=[foo1.Bar,foo1.Bar] = <function1>

scala> implicitly[foo1.Bar =:= foo2.Bar] // Not OK: unequal types
<console>:11: error: Cannot prove that foo1.Bar =:= foo2.Bar.
              implicitly[foo1.Bar =:= foo2.Bar]

Theo quan điểm của tôi, những điều trên đã đủ để trả lời câu hỏi "Scala có phải là một ngôn ngữ được gõ phụ thuộc không?" tích cực: rõ ràng là ở đây chúng ta có các kiểu được phân biệt bởi các giá trị là tiền tố của chúng.

Tuy nhiên, người ta thường phản đối rằng Scala không phải là một ngôn ngữ loại phụ thuộc "hoàn toàn" bởi vì nó không có tổng phụ thuộc và loại sản phẩm như được tìm thấy trong Agda hoặc Coq hoặc Idris như là bản chất. Tôi nghĩ rằng điều này phản ánh sự cố định về hình thức so với các nguyên tắc cơ bản ở một mức độ nào đó, tuy nhiên, tôi sẽ cố gắng và cho thấy rằng Scala gần với những ngôn ngữ khác hơn rất nhiều so với những gì thường được thừa nhận.

Mặc dù theo thuật ngữ, các kiểu tổng phụ thuộc (còn được gọi là kiểu Sigma) chỉ đơn giản là một cặp giá trị trong đó kiểu của giá trị thứ hai phụ thuộc vào giá trị đầu tiên. Điều này có thể đại diện trực tiếp trong Scala,

scala> trait Sigma {
     |   val foo: Foo
     |   val bar: foo.Bar
     | }
defined trait Sigma

scala> val sigma = new Sigma {
     |   val foo = foo1
     |   val bar = new foo.Bar
     | }
sigma: java.lang.Object with Sigma{val bar: this.foo.Bar} = $anon$1@e3fabd8

và trên thực tế, đây là một phần quan trọng của mã hóa các loại phương thức phụ thuộc cần thiết để thoát khỏi 'Bakery of Doom' trong Scala trước 2.10 (hoặc sớm hơn thông qua tùy chọn trình biên dịch Scala thử nghiệm -Ydependent-method type).

Các loại sản phẩm phụ thuộc (hay còn gọi là các loại Pi) về cơ bản là các hàm từ giá trị đến loại. Chúng là chìa khóa để biểu diễn các vectơ có kích thước tĩnh và các con áp phích khác cho các ngôn ngữ lập trình được định kiểu phụ thuộc. Chúng ta có thể mã hóa các kiểu Pi trong Scala bằng cách sử dụng kết hợp các kiểu phụ thuộc đường dẫn, kiểu singleton và các tham số ngầm định. Đầu tiên, chúng tôi xác định một đặc điểm sẽ biểu diễn một hàm từ giá trị kiểu T sang kiểu U,

scala> trait Pi[T] { type U }
defined trait Pi

Chúng ta không thể xác định một phương thức đa hình sử dụng kiểu này,

scala> def depList[T](t: T)(implicit pi: Pi[T]): List[pi.U] = Nil
depList: [T](t: T)(implicit pi: Pi[T])List[pi.U]

(lưu ý việc sử dụng kiểu phụ thuộc đường dẫn pi.Utrong kiểu kết quả List[pi.U]). Cho một giá trị kiểu T, hàm này sẽ trả về một danh sách (n trống) các giá trị của kiểu tương ứng với giá trị T cụ thể đó.

Bây giờ chúng ta hãy xác định một số giá trị phù hợp và các nhân chứng ngầm cho các mối quan hệ chức năng mà chúng ta muốn nắm giữ,

scala> object Foo
defined module Foo

scala> object Bar
defined module Bar

scala> implicit val fooInt = new Pi[Foo.type] { type U = Int }
fooInt: java.lang.Object with Pi[Foo.type]{type U = Int} = $anon$1@60681a11

scala> implicit val barString = new Pi[Bar.type] { type U = String }
barString: java.lang.Object with Pi[Bar.type]{type U = String} = $anon$1@187602ae

Và bây giờ đây là chức năng sử dụng kiểu Pi của chúng tôi đang hoạt động,

scala> depList(Foo)
res2: List[fooInt.U] = List()

scala> depList(Bar)
res3: List[barString.U] = List()

scala> implicitly[res2.type <:< List[Int]]
res4: <:<[res2.type,List[Int]] = <function1>

scala> implicitly[res2.type <:< List[String]]
<console>:19: error: Cannot prove that res2.type <:< List[String].
              implicitly[res2.type <:< List[String]]
                    ^

scala> implicitly[res3.type <:< List[String]]
res6: <:<[res3.type,List[String]] = <function1>

scala> implicitly[res3.type <:< List[Int]]
<console>:19: error: Cannot prove that res3.type <:< List[Int].
              implicitly[res3.type <:< List[Int]]

(lưu ý rằng ở đây chúng tôi sử dụng <:<toán tử chứng kiến ​​kiểu phụ của Scala thay =:=vì bởi vì res2.typeres3.typelà các kiểu singleton và do đó chính xác hơn các kiểu mà chúng tôi đang xác minh trên RHS).

Tuy nhiên, trong thực tế, trong Scala, chúng tôi sẽ không bắt đầu bằng cách mã hóa các loại Sigma và Pi và sau đó tiếp tục từ đó như chúng tôi làm trong Agda hoặc Idris. Thay vào đó, chúng tôi sẽ sử dụng các kiểu phụ thuộc đường dẫn, kiểu singleton và hàm ý trực tiếp. Bạn có thể tìm thấy rất nhiều ví dụ về cách điều này diễn ra trong không định hình: các loại có kích thước , bản ghi có thể mở rộng , danh sách HL toàn diện , loại bỏ bảng mẫu của bạn , Dây kéo chung , v.v.

Sự phản đối duy nhất còn lại mà tôi có thể thấy là trong mã hóa kiểu Pi ở trên, chúng ta yêu cầu các kiểu singleton của các giá trị phụ thuộc phải có thể biểu đạt được. Thật không may trong Scala, điều này chỉ có thể thực hiện được đối với các giá trị của kiểu tham chiếu chứ không phải đối với các giá trị của kiểu không tham chiếu (ví dụ như int). Đây là một sự xấu hổ, nhưng không phải là một khó khăn nội tại: kiểm tra loại Scala của đại diện các kiểu singleton các giá trị phi tài liệu tham khảo nội bộ, và đã có một vài các thí nghiệm trong việc đưa ra chúng trực tiếp có thể biểu. Trong thực tế, chúng ta có thể giải quyết vấn đề này với một bảng mã cấp kiểu khá chuẩn của các số tự nhiên .

Trong mọi trường hợp, tôi không nghĩ rằng giới hạn tên miền nhỏ này có thể được sử dụng như một sự phản đối trạng thái của Scala như một ngôn ngữ được đánh máy phụ thuộc. Nếu đúng như vậy, thì điều tương tự cũng có thể được nói cho Dependent ML (chỉ cho phép phụ thuộc vào các giá trị số tự nhiên), đây sẽ là một kết luận kỳ lạ.


8
Miles, cảm ơn vì câu trả lời rất chi tiết này. Tuy nhiên, tôi hơi tò mò về một điều. Không có ví dụ nào của bạn thoạt nhìn có vẻ đặc biệt khó diễn đạt bằng Haskell; sau đó bạn có tuyên bố rằng Haskell cũng là một ngôn ngữ được đánh máy phụ thuộc không?
Jonathan Sterling

8
Tôi đã phản đối vì tôi không thể phân biệt các kỹ thuật ở đây về bản chất với các kỹ thuật được mô tả trong "Làm giả" citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.22.2636 của McBride - tức là đây là những cách mô phỏng loại phụ thuộc, không cung cấp trực tiếp.
sclv

2
@sclv Tôi nghĩ rằng bạn đã bỏ lỡ rằng Scala có các kiểu phụ thuộc mà không có bất kỳ hình thức mã hóa nào: hãy xem ví dụ đầu tiên ở trên. Bạn hoàn toàn đúng khi mã hóa các loại Pi của tôi sử dụng một số kỹ thuật tương tự như giấy của Connor, nhưng từ chất nền đã bao gồm các loại phụ thuộc đường dẫn và các loại singleton.
Miles Sabin

4
Không. Chắc chắn bạn có thể có các kiểu gắn với các đối tượng (đây là hệ quả của các đối tượng dưới dạng mô-đun). Nhưng bạn không thể tính toán các loại này mà không sử dụng các nhân chứng cấp giá trị. Trên thực tế =: = bản thân nó là một nhân chứng cấp giá trị! Bạn vẫn đang giả mạo nó, giống như bạn phải làm trong Haskell, hoặc có lẽ hơn thế.
sclv

9
Scala's =: = không phải là cấp giá trị, đó là một phương thức tạo kiểu - giá trị cho điều đó ở đây: github.com/scala/scala/blob/v2.10.3/src/library/scala/… và có vẻ như không đặc biệt khác với một nhân chứng cho một mệnh đề bình đẳng trong các ngôn ngữ được đánh máy phụ thuộc như Agda và Idris: refl. (Xem www2.tcs.ifi.lmu.de/~abel/Equality.pdf phần 2 và eb.host.cs.st-andrews.ac.uk/writings/idris-tutorial.pdf phần 8.1, tương ứng.)
pdxleif 14/1113

6

Tôi cho rằng đó là vì (như tôi biết từ kinh nghiệm, đã sử dụng các kiểu phụ thuộc trong trợ lý bằng chứng Coq, hỗ trợ đầy đủ chúng nhưng vẫn không thuận tiện lắm) các kiểu phụ thuộc là một tính năng ngôn ngữ lập trình rất nâng cao, điều này thực sự khó làm đúng - và có thể gây ra sự phức tạp theo cấp số nhân trong thực tế. Chúng vẫn là một chủ đề nghiên cứu khoa học máy tính.


bạn có thể vui lòng cung cấp cho tôi một số nền tảng lý thuyết về các loại phụ thuộc (một liên kết có lẽ)?
Ashkan Kh. Nazary

3
@ ashy_32bit nếu bạn có thể truy cập vào "Chủ đề nâng cao về loại và ngôn ngữ lập trình" của Benjamin Pierce, thì có một chương trong đó giới thiệu hợp lý về các loại phụ thuộc. Bạn cũng có thể đọc một số bài báo của Conor McBride, người đặc biệt quan tâm đến các kiểu phụ thuộc trong thực tế hơn là lý thuyết.
iainmcgin

3

Tôi tin rằng các kiểu phụ thuộc vào đường dẫn của Scala chỉ có thể đại diện cho các kiểu Σ, chứ không phải các kiểu Π. Điều này:

trait Pi[T] { type U }

không hẳn là kiểu Π. Theo định nghĩa, Π-type, hay tích phụ thuộc, là một hàm mà kiểu kết quả phụ thuộc vào giá trị đối số, đại diện cho định lượng phổ quát, tức là ∀x: A, B (x). Tuy nhiên, trong trường hợp trên, nó chỉ phụ thuộc vào kiểu T, chứ không phụ thuộc vào một số giá trị của kiểu này. Bản thân đặc điểm Pi là một kiểu Σ, một định lượng tồn tại, tức là ∃x: A, B (x). Sự tự tham chiếu của đối tượng trong trường hợp này hoạt động như một biến định lượng. Tuy nhiên, khi được truyền vào dưới dạng tham số ngầm định, nó giảm thành một hàm kiểu thông thường, vì nó được giải quyết theo kiểu khôn ngoan. Mã hóa cho sản phẩm phụ thuộc trong Scala có thể giống như sau:

trait Sigma[T] {
  val x: T
  type U //can depend on x
}

// (t: T) => (∃ mapping(x, U), x == t) => (u: U); sadly, refinement won't compile
def pi[T](t: T)(implicit mapping: Sigma[T] { val x = t }): mapping.U 

Phần còn thiếu ở đây là khả năng hạn chế tĩnh trường x thành giá trị kỳ vọng t, tạo thành một phương trình biểu thị thuộc tính của tất cả các giá trị có kiểu T. Cùng với Σ-type của chúng ta, được logic được hình thành, trong đó phương trình của chúng ta là một định lý cần được chứng minh.

Một lưu ý nhỏ, trong trường hợp thực tế, định lý có thể rất tầm thường, đến mức nó không thể tự động rút ra từ mã hoặc giải được mà không cần nỗ lực đáng kể. Người ta thậm chí có thể xây dựng Giả thuyết Riemann theo cách này, chỉ để tìm ra chữ ký không thể thực hiện mà không thực sự chứng minh nó, lặp đi lặp lại mãi mãi hoặc ném ra một ngoại lệ.


1
Miles Sabin ở trên đã chỉ ra một ví dụ về việc sử dụng Piđể tạo các loại tùy thuộc vào giá trị.
missfaktor

Trong ví dụ, depListtrích xuất loại Utừ Pi[T], được chọn cho loại (không phải giá trị) của t. Kiểu này chỉ xảy ra là kiểu singleton, hiện có sẵn trên các đối tượng singleton của Scala và đại diện cho các giá trị chính xác của chúng. Ví dụ tạo một triển khai của Pimỗi kiểu đối tượng singleton, do đó ghép nối kiểu với giá trị như trong kiểu Σ. Mặt khác, Π-type là một công thức phù hợp với cấu trúc của tham số đầu vào của nó. Có thể, Scala không có chúng vì kiểu Π yêu cầu mọi kiểu tham số phải là GADT và Scala không phân biệt GADT với các kiểu khác.
P. Frolov

Được rồi, tôi hơi bối rối. pi.UTrong ví dụ của Miles sẽ không được tính là loại phụ thuộc? Đó là giá trị pi.
missfaktor

2
Nó thực sự được tính là kiểu phụ thuộc, nhưng có các hương vị khác nhau của những loại đó: Σ-type ("tồn tại x sao cho P (x)", logic-khôn ngoan) và Π-type ("cho mọi x, P (x)") . Như bạn đã lưu ý, loại pi.Uphụ thuộc vào giá trị của pi. Vấn đề ngăn cản việc trait Pi[T]trở thành kiểu Π là chúng ta không thể làm cho nó phụ thuộc vào giá trị của một đối số tùy ý (ví dụ: tin depList) mà không nâng đối số đó ở cấp kiểu.
P. Frolov

1

Câu hỏi là về việc sử dụng tính năng gõ phụ thuộc trực tiếp hơn và, theo ý kiến ​​của tôi, sẽ có lợi ích khi có cách tiếp cận gõ phụ thuộc trực tiếp hơn so với những gì Scala cung cấp.
Các câu trả lời hiện tại cố gắng tranh luận câu hỏi trên cấp độ lý thuyết loại. Tôi muốn thực hiện nó một cách thực dụng hơn. Điều này có thể giải thích tại sao mọi người được phân chia theo mức độ hỗ trợ của các loại phụ thuộc trong ngôn ngữ Scala. Chúng ta có thể có những định nghĩa hơi khác nhau trong tâm trí. (không có nghĩa là một đúng và một sai).

Đây không phải là một nỗ lực để trả lời câu hỏi việc biến Scala thành một thứ giống như Idris sẽ dễ dàng như thế nào (tôi tưởng tượng rất khó) hoặc viết một thư viện cung cấp hỗ trợ trực tiếp hơn cho các khả năng giống như Idris (như singletonscố gắng trở thành Haskell).

Thay vào đó, tôi muốn nhấn mạnh sự khác biệt thực dụng giữa Scala và một ngôn ngữ như Idris.
Các bit mã cho biểu thức mức giá trị và kiểu là gì? Idris sử dụng cùng một mã, Scala sử dụng mã rất khác nhau.

Scala (tương tự như Haskell) có thể mã hóa nhiều phép tính mức kiểu. Điều này được hiển thị bởi các thư viện như shapeless. Các thư viện này làm điều đó bằng cách sử dụng một số thủ thuật thực sự ấn tượng và thông minh. Tuy nhiên, mã cấp kiểu của chúng (hiện tại) khá khác với các biểu thức cấp giá trị (tôi thấy rằng khoảng cách đó có phần gần hơn trong Haskell). Idris cho phép sử dụng biểu thức mức giá trị trên mức kiểu AS IS.

Lợi ích rõ ràng là sử dụng lại mã (bạn không cần phải viết mã các biểu thức cấp kiểu riêng biệt với cấp giá trị nếu bạn cần chúng ở cả hai nơi). Nó sẽ dễ dàng hơn để viết mã mức giá trị. Nó sẽ dễ dàng hơn để không phải đối phó với các vụ hack như singleleton (chưa kể đến chi phí hiệu suất). Bạn không cần phải học hai thứ mà bạn học một thứ. Ở mức độ thực dụng, chúng ta sẽ cần ít khái niệm hơn. Gõ từ đồng nghĩa, họ kiểu, hàm, ... còn chỉ hàm thì sao? Theo tôi, lợi ích hợp nhất này đi sâu hơn nhiều và còn hơn cả sự tiện lợi về mặt cú pháp.

Xem xét mã đã xác minh. Xem:
https://github.com/idris-lang/Idris-dev/blob/v1.3.0/libs/contrib/Interfaces/Verified.idr
Trình kiểm tra loại xác minh bằng chứng về luật đơn nguyên / functor / áp dụng và các bằng chứng là thực tế triển khai đơn nguyên / chức năng / ứng dụng và không phải một số loại tương đương mức được mã hóa có thể giống nhau hoặc không giống nhau. Câu hỏi lớn là chúng ta đang chứng minh điều gì?

Tôi cũng có thể làm như vậy bằng cách sử dụng các thủ thuật mã hóa thông minh (xem phần sau cho phiên bản Haskell, tôi chưa thấy một phần cho Scala)
https://blog.jle.im/entry/verified-instances-in-haskell.html https://blog.jle.im/entry/verified-instances-in-haskell.html
https:// github.com/rpeszek/IdrisTddNotes/wiki/Play_FunctorLaws
trừ các loại được quá phức tạp mà rất khó để nhìn thấy các định luật, các từ ngữ mức giá trị được chuyển đổi (tự động nhưng vẫn) để gõ những mức và bạn cần phải tin tưởng chuyển đổi đó là tốt . Có chỗ cho sai sót trong tất cả những điều này mà loại bỏ mục đích của trình biên dịch hoạt động như một trợ lý chứng minh.

(EDITED 2018.8.10) Nói về hỗ trợ bằng chứng, đây là một điểm khác biệt lớn giữa Idris và Scala. Không có gì trong Scala (hoặc Haskell) có thể ngăn cản việc viết các bằng chứng phân kỳ:

case class Void(underlying: Nothing) extends AnyVal //should be uninhabited
def impossible() : Void = impossible()

trong khi Idris có totaltừ khóa ngăn mã như thế này biên dịch.

Một thư viện Scala cố gắng thống nhất giá trị và mã cấp kiểu (như Haskell singletons) sẽ là một thử nghiệm thú vị cho sự hỗ trợ của Scala đối với các kiểu phụ thuộc. Thư viện như vậy có thể được thực hiện tốt hơn nhiều trong Scala vì các kiểu phụ thuộc vào đường dẫn không?

Tôi còn quá mới với Scala để tự trả lời câu hỏi đó.

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.