Tại sao không được gõ phụ thuộc?


161

Tôi đã thấy một số nguồn tin lặp lại ý kiến ​​rằng "Haskell đang dần trở thành một ngôn ngữ được gõ phụ thuộc". Hàm ý dường như là với ngày càng nhiều phần mở rộng ngôn ngữ, Haskell đang trôi theo hướng chung đó, nhưng vẫn chưa có.

Về cơ bản có hai điều tôi muốn biết. Đầu tiên, khá đơn giản, "thực sự một ngôn ngữ được gõ phụ thuộc" nghĩa là gì? (Hy vọng rằng không quá kỹ thuật về nó.)

Câu hỏi thứ hai là ... nhược điểm là gì? Ý tôi là, mọi người biết chúng ta đang đi theo hướng đó, vì vậy phải có một số lợi thế cho nó. Tuy nhiên, chúng tôi chưa có ở đó, vì vậy phải có một số nhược điểm ngăn chặn mọi người đi tất cả các cách. Tôi có ấn tượng rằng vấn đề là sự gia tăng phức tạp. Nhưng, không thực sự hiểu gõ phụ thuộc là gì, tôi không biết chắc chắn.

Những gì tôi làm ai biết được rằng mỗi khi tôi bắt đầu đọc về một ngôn ngữ lập trình lệ thuộc, đánh máy, văn bản là hoàn toàn không thể hiểu ... Có lẽ đó là vấn đề. (?)


10
Nói một cách đơn giản, bạn có thể viết các loại phụ thuộc vào các điều khoản (tính toán). Điều này là đủ để chỉ định các loại về mọi khía cạnh của chương trình của bạn và do đó có nghĩa là hệ thống loại có khả năng đặc tả chương trình đầy đủ. Vấn đề là bởi vì các loại phụ thuộc vào tính toán, việc kiểm tra loại rất khó thực hiện (nói chung là không thể).
GManNickG

27
@GManNickG: Kiểm tra loại là hoàn toàn có thể. Suy luận kiểu là một vấn đề khác, nhưng một lần nữa, các phần mở rộng khác nhau của GHC từ lâu đã từ bỏ ý tưởng rằng có thể suy ra tất cả các loại.
CA McCann

7
Nếu tôi hiểu chính xác, nhược điểm là việc gõ phụ thuộc đúng (ví dụ, theo cách vừa có thể sử dụng vừa có cơ sở) là khó , và chúng ta vẫn chưa biết cách nào.
sắp tới

1
@CAMcCann: Vâng, lỗi của tôi.
GManNickG

4
Tôi không nghĩ rằng ai đó đã chỉ ra một nhược điểm thực dụng lớn: viết bằng chứng rằng tất cả các mã của bạn là chính xác là khá tẻ nhạt. Vì bạn không thể tự động thực hiện suy luận kiểu (tương ứng với định lý chứng minh trong logic "hella mạnh"), bạn phải viết chú thích cho chương trình của mình dưới dạng bằng chứng. Điều này rõ ràng trở nên khó chịu và khó thực hiện sau một thời gian, đặc biệt là đối với phép thuật đơn điệu phức tạp hơn mà mọi người thường làm trong Haskell. Gần nhất mà chúng ta đến vào những ngày này là các ngôn ngữ thực hiện hầu hết điều này cho chúng ta hoặc cung cấp cho chúng ta một bộ nguyên thủy tốt.
Kristopher Micinski

Câu trả lời:


21

Việc gõ phụ thuộc thực sự chỉ là sự thống nhất giữa các mức giá trị và loại, do đó bạn có thể tham số hóa các giá trị trên các loại (đã có thể với các loại loại và đa hình tham số trong Haskell) và bạn có thể tham số các loại trên các giá trị (không, nói đúng, có thể có trong Haskell , mặc dù DataKindsđược rất gần).

Chỉnh sửa: Rõ ràng, từ thời điểm này trở đi, tôi đã sai (xem bình luận của @ pigworker). Tôi sẽ lưu giữ phần còn lại của điều này như một bản ghi chép về những huyền thoại tôi đã được cho ăn. : P


Vấn đề với việc chuyển sang gõ phụ thuộc hoàn toàn, từ những gì tôi đã nghe, là nó sẽ phá vỡ giới hạn pha giữa mức độ loại và giá trị cho phép Haskell được biên dịch thành mã máy hiệu quả với các loại bị xóa. Với trình độ công nghệ hiện tại của chúng tôi, một ngôn ngữ được gõ phụ thuộc phải thông qua một trình thông dịch tại một thời điểm nào đó (ngay lập tức hoặc sau khi được biên dịch thành mã byte được gõ phụ thuộc hoặc tương tự).

Đây không nhất thiết là một hạn chế cơ bản, nhưng cá nhân tôi không biết về bất kỳ nghiên cứu hiện tại nào có vẻ hứa hẹn về vấn đề này nhưng điều đó chưa được đưa vào GHC. Nếu ai khác biết nhiều hơn, tôi sẽ rất vui khi được sửa chữa.


46
Những gì bạn nói gần như hoàn toàn sai. Tôi không hoàn toàn đổ lỗi cho bạn: nó lặp đi lặp lại những huyền thoại tiêu chuẩn. Ngôn ngữ của Edwin Brady, Idris, thực hiện việc xóa kiểu (vì không có hành vi thời gian chạy nào phụ thuộc vào các loại) và tạo ra một bộ mã hóa siêu kết hợp được nâng lên khá chuẩn từ đó sử dụng các kỹ thuật máy G-stock.
thợ lợn

3
Như một lưu ý phụ, gần đây ai đó đã chỉ cho tôi bài báo này . Từ những gì tôi có thể nói, nó sẽ khiến Haskell bị loại một cách phụ thuộc (nghĩa là ngôn ngữ cấp độ loại sẽ được gõ một cách phụ thuộc), gần giống như tôi có thể thấy chúng ta sẽ sớm nhận được.
Ngọn lửa của Ptharien

8
Đúng, bài báo đó hầu hết đều chỉ ra cách tạo ra các loại phụ thuộc vào công cụ cấp độ loại (và để loại bỏ sự phân biệt loại / loại). Một theo dõi hợp lý, đã được thảo luận, là cho phép các loại hàm phụ thuộc thực tế, nhưng hạn chế các đối số của chúng đối với đoạn ngôn ngữ có thể tồn tại trong cả hai lớp giá trị và loại (bây giờ không cần thiết nhờ quảng bá kiểu dữ liệu). Điều đó sẽ loại bỏ sự cần thiết cho việc xây dựng đơn lẻ hiện đang làm cho việc "giả mạo" trở nên phức tạp hơn mong muốn. Chúng tôi đang dần dần gần hơn với thực tế.
thợ lợn

13
Có rất nhiều câu hỏi thực dụng, trang bị thêm các loại phụ thuộc vào Haskell. Khi chúng ta đã có dạng không gian hàm phụ thuộc bị hạn chế này, chúng ta vẫn phải đối mặt với câu hỏi làm thế nào để phóng to đoạn ngôn ngữ giá trị được phép ở cấp độ loại và lý thuyết phương trình của nó là gì (như chúng ta muốn 2 + 2 được 4, và như vậy). Có rất nhiều vấn đề rắc rối (ví dụ: phía dưới) mà các ngôn ngữ được gõ phụ thuộc từ đầu được thiết kế cách xa nhau.
thợ lợn

2
@pigworker Có một tập hợp con sạch của Haskell đó là tổng số không? Nếu vậy, chúng ta không thể sử dụng điều đó cho "đoạn ngôn ngữ có thể tồn tại ở cả hai lớp giá trị và loại"? Nếu không, cần những gì để sản xuất một?
Ngọn lửa của Ptharien

223

Gõ phụ thuộc Haskell, bây giờ?

Haskell, ở một mức độ nhỏ, là một ngôn ngữ được gõ phụ thuộc. Có một khái niệm về dữ liệu cấp loại, giờ đây được nhập một cách hợp lý hơn nhờ vào DataKinds, và có một số phương tiện ( GADTs) để đưa ra biểu diễn thời gian chạy cho dữ liệu cấp loại. Do đó, các giá trị của công cụ thời gian chạy hiển thị một cách hiệu quả trong các loại , đó là ý nghĩa của một ngôn ngữ được gõ một cách phụ thuộc.

Các kiểu dữ liệu đơn giản được thăng cấp đến mức loại, do đó các giá trị mà chúng chứa có thể được sử dụng trong các loại. Do đó, ví dụ điển hình

data Nat = Z | S Nat

data Vec :: Nat -> * -> * where
  VNil   :: Vec Z x
  VCons  :: x -> Vec n x -> Vec (S n) x

trở nên có thể, và với nó, các định nghĩa như

vApply :: Vec n (s -> t) -> Vec n s -> Vec n t
vApply VNil         VNil         = VNil
vApply (VCons f fs) (VCons s ss) = VCons (f s) (vApply fs ss)

cái gì là tốt. Lưu ý rằng độ dài nlà một thứ hoàn toàn tĩnh trong hàm đó, đảm bảo rằng các vectơ đầu vào và đầu ra có cùng độ dài, mặc dù độ dài đó không có vai trò gì trong việc thực hiện vApply. Ngược lại, nó nhiều phức tạp hơn (ví dụ, không thể) để thực hiện các chức năng mà làm cho ncác bản sao của một định x(đó sẽ là puređể vApply's <*>)

vReplicate :: x -> Vec n x

bởi vì điều quan trọng là phải biết có bao nhiêu bản sao để tạo ra trong thời gian chạy. Nhập đơn.

data Natty :: Nat -> * where
  Zy :: Natty Z
  Sy :: Natty n -> Natty (S n)

Đối với bất kỳ loại có thể quảng bá nào, chúng ta có thể xây dựng họ singleton, được lập chỉ mục cho loại được quảng cáo, có các bản sao thời gian chạy của các giá trị của nó. Natty nlà loại bản sao thời gian chạy của cấp độ loại n :: Nat. Bây giờ chúng ta có thể viết

vReplicate :: Natty n -> x -> Vec n x
vReplicate Zy     x = VNil
vReplicate (Sy n) x = VCons x (vReplicate n x)

Vì vậy, ở đó bạn có một giá trị cấp độ được chuyển thành giá trị thời gian chạy: kiểm tra bản sao thời gian chạy tinh chỉnh kiến ​​thức tĩnh của giá trị cấp độ loại. Mặc dù các thuật ngữ và loại được tách ra, chúng ta có thể làm việc theo cách gõ phụ thuộc bằng cách sử dụng cấu trúc đơn như một loại nhựa epoxy, tạo liên kết giữa các pha. Đó là một chặng đường dài từ việc cho phép các biểu thức thời gian chạy tùy ý trong các loại, nhưng nó không là gì cả.

Khó chịu là gì? Cái gì còn thiếu?

Hãy tạo một chút áp lực cho công nghệ này và xem những gì bắt đầu chao đảo. Chúng ta có thể có ý tưởng rằng các singletons nên được quản lý một cách ngầm định hơn một chút

class Nattily (n :: Nat) where
  natty :: Natty n
instance Nattily Z where
  natty = Zy
instance Nattily n => Nattily (S n) where
  natty = Sy natty

cho phép chúng tôi viết, nói,

instance Nattily n => Applicative (Vec n) where
  pure = vReplicate natty
  (<*>) = vApply

Điều đó hoạt động, nhưng bây giờ có nghĩa là Natloại ban đầu của chúng tôi đã sinh ra ba bản sao: một loại, một gia đình độc thân và một lớp đơn. Chúng tôi có một quy trình khá lộn xộn để trao đổi Natty ncác giá trị và Nattily ntừ điển rõ ràng . Hơn nữa, Nattykhông phải là Nat: chúng ta có một số loại phụ thuộc vào các giá trị thời gian chạy, nhưng không phải ở loại mà chúng ta nghĩ đến đầu tiên. Không có ngôn ngữ gõ hoàn toàn phụ thuộc làm cho loại phụ thuộc này phức tạp!

Trong khi đó, mặc dù Natcó thể được thăng chức, Veckhông thể. Bạn không thể lập chỉ mục theo loại được lập chỉ mục. Hoàn toàn dựa trên các ngôn ngữ được gõ phụ thuộc không có giới hạn nào như vậy, và trong sự nghiệp của tôi là một cuộc phô trương phụ thuộc, tôi đã học cách đưa các ví dụ về lập chỉ mục hai lớp trong các cuộc nói chuyện của mình, chỉ để dạy cho những người đã lập chỉ mục một lớp khó nhưng có thể không mong đợi tôi xếp lại như một ngôi nhà của những lá bài. Có vấn đề gì vậy? Bình đẳng. Các GADT hoạt động bằng cách dịch các ràng buộc mà bạn đạt được hoàn toàn khi bạn cung cấp cho một hàm tạo một kiểu trả về cụ thể thành các yêu cầu phương trình rõ ràng. Như thế này.

data Vec (n :: Nat) (x :: *)
  = n ~ Z => VNil
  | forall m. n ~ S m => VCons x (Vec m x)

Trong mỗi hai phương trình của chúng tôi, cả hai bên đều có loại Nat.

Bây giờ hãy thử bản dịch tương tự cho một cái gì đó được lập chỉ mục trên các vectơ.

data InVec :: x -> Vec n x -> * where
  Here :: InVec z (VCons z zs)
  After :: InVec z ys -> InVec z (VCons y ys)

trở thành

data InVec (a :: x) (as :: Vec n x)
  = forall m z (zs :: Vec x m). (n ~ S m, as ~ VCons z zs) => Here
  | forall m y z (ys :: Vec x m). (n ~ S m, as ~ VCons y ys) => After (InVec z ys)

và bây giờ chúng ta hình thành các ràng buộc phương trình giữa as :: Vec n xVCons z zs :: Vec (S m) xnơi hai bên có các loại khác nhau về mặt cú pháp (nhưng có thể chứng minh bằng nhau). Lõi GHC hiện không được trang bị cho một khái niệm như vậy!

Còn thiếu gì nữa không? Chà, hầu hết Haskell bị thiếu ở cấp độ loại. Ngôn ngữ của các thuật ngữ mà bạn có thể quảng bá chỉ có các biến và các hàm tạo không phải GADT, thực sự. Khi bạn đã có những thứ đó, type familymáy móc cho phép bạn viết các chương trình cấp độ: một số trong số đó có thể khá giống các chức năng bạn sẽ xem xét viết ở cấp độ thuật ngữ (ví dụ: trang bị Natthêm, vì vậy bạn có thể đưa ra một loại tốt để nối thêm Vec) , nhưng đó chỉ là sự trùng hợp!

Một điều còn thiếu, trong thực tế, là một thư viện sử dụng các khả năng mới của chúng tôi để lập chỉ mục các loại theo các giá trị. Làm gì FunctorMonadtrở thành trong thế giới mới dũng cảm này? Tôi đang nghĩ về nó, nhưng vẫn còn nhiều việc phải làm.

Chạy các chương trình cấp độ

Haskell, giống như hầu hết các ngôn ngữ lập trình được gõ phụ thuộc, có hai ngữ nghĩa hoạt động. Có cách hệ thống thời gian chạy chạy các chương trình (chỉ biểu thức đóng, sau khi xóa kiểu, được tối ưu hóa cao) và sau đó là cách trình đánh máy chạy chương trình (gia đình kiểu của bạn, "Prolog lớp loại" của bạn, với biểu thức mở). Đối với Haskell, bạn thường không kết hợp cả hai, vì các chương trình đang được thực thi bằng các ngôn ngữ khác nhau. Các ngôn ngữ được gõ phụ thuộc có các mô hình thực thi tĩnh và thời gian chạy riêng biệt cho cùng một ngôn ngữ của các chương trình, nhưng đừng lo, mô hình thời gian chạy vẫn cho phép bạn thực hiện xóa và thực sự, xóa bằng chứng: đó là trích xuất của Coqcơ chế mang lại cho bạn; đó ít nhất là những gì trình biên dịch của Edwin Brady thực hiện (mặc dù Edwin xóa các giá trị trùng lặp không cần thiết, cũng như các loại và bằng chứng). Sự phân biệt pha có thể không còn là sự phân biệt của thể loại cú pháp nữa, nhưng nó vẫn sống và tốt.

Các ngôn ngữ được gõ một cách phụ thuộc, là tổng số, cho phép người đánh máy chạy các chương trình miễn phí khỏi nỗi sợ bất cứ điều gì tồi tệ hơn là chờ đợi lâu. Khi Haskell trở nên phụ thuộc nhiều hơn, chúng ta phải đối mặt với câu hỏi mô hình thực thi tĩnh của nó sẽ là gì? Một cách tiếp cận có thể là hạn chế thực thi tĩnh đối với các hàm tổng, cho phép chúng ta có cùng quyền tự do chạy, nhưng có thể buộc chúng ta tạo ra sự khác biệt (ít nhất là đối với mã cấp độ loại) giữa dữ liệu và codata, để chúng ta có thể biết liệu có thực thi chấm dứt hoặc năng suất. Nhưng đó không phải là cách tiếp cận duy nhất. Chúng tôi có thể tự do lựa chọn một mô hình thực thi yếu hơn nhiều, miễn cưỡng chạy các chương trình, với chi phí tạo ra ít phương trình được đưa ra chỉ bằng tính toán. Và thực tế, đó là những gì GHC thực sự làm. Các quy tắc gõ cho lõi GHC không đề cập đến việc chạy chương trình, nhưng chỉ để kiểm tra bằng chứng cho phương trình. Khi dịch vào lõi, bộ giải ràng buộc của GHC cố chạy các chương trình cấp loại của bạn, tạo ra một dấu vết nhỏ bằng chứng cho thấy một biểu thức đã cho bằng với dạng thông thường của nó. Phương pháp tạo bằng chứng này hơi khó đoán và không thể tránh khỏi hoàn toàn: chẳng hạn, nó chống lại sự đệ quy trông đáng sợ, và điều đó có lẽ là khôn ngoan. Một điều chúng ta không cần phải lo lắng là việc thực hiện các IO tính toán trong máy đánh chữ: hãy nhớ rằng máy đánh chữ không phải mang launchMissilesý nghĩa tương tự như hệ thống thời gian chạy!

Văn hóa Hindley-Milner

Hệ thống loại Hindley-Milner đạt được sự trùng hợp thực sự tuyệt vời của bốn sự khác biệt, với tác dụng phụ đáng tiếc về mặt văn hóa mà nhiều người không thể thấy sự khác biệt giữa sự khác biệt và cho rằng sự trùng hợp là không thể tránh khỏi! Tôi đang nói về cái gì vậy?

  • điều khoản vs loại
  • những điều được viết một cách rõ ràng vs điều ngầm bằng văn bản
  • hiện diện tại thời gian chạy vs tẩy xoá trước khi thời gian chạy
  • trừu tượng không phụ thuộc so với định lượng phụ thuộc

Chúng ta thường viết các thuật ngữ và để các loại được suy luận ... và sau đó bị xóa. Chúng ta thường sử dụng để định lượng các biến loại với sự trừu tượng hóa và ứng dụng loại tương ứng diễn ra âm thầm và tĩnh.

Bạn không cần phải rẽ quá xa vani Hindley-Milner trước những khác biệt ra khỏi sự liên kết, và đó là không có điều xấu . Để bắt đầu, chúng ta có thể có nhiều loại thú vị hơn nếu chúng ta sẵn sàng viết chúng ở một vài nơi. Trong khi đó, chúng ta không phải viết từ điển lớp loại khi chúng ta sử dụng các hàm quá tải, nhưng những từ điển đó chắc chắn có mặt (hoặc nội tuyến) vào thời gian chạy. Trong các ngôn ngữ được gõ phụ thuộc, chúng tôi hy vọng sẽ xóa nhiều hơn chỉ các loại trong thời gian chạy, nhưng (như với các loại loại) rằng một số giá trị được suy luận ngầm sẽ không bị xóa. Ví dụ, vReplicateđối số số thường được suy ra từ loại vectơ mong muốn, nhưng chúng ta vẫn cần biết nó vào thời gian chạy.

Những lựa chọn thiết kế ngôn ngữ nào chúng ta nên xem xét bởi vì những sự trùng hợp này không còn giữ được nữa? Ví dụ, có đúng là Haskell không cung cấp cách nào để khởi tạo một bộ forall x. tđịnh lượng một cách rõ ràng không? Nếu người đánh máy không thể đoán xbằng cách unifiying t, chúng ta không có cách nào khác để nói những gì xphải được.

Rộng hơn, chúng ta không thể coi "suy luận kiểu" là một khái niệm nguyên khối mà chúng ta có hoặc không có gì cả. Để bắt đầu, chúng ta cần tách ra khía cạnh "khái quát hóa" (quy tắc "cho phép" của Milner), điều này phụ thuộc rất nhiều vào việc hạn chế loại nào tồn tại để đảm bảo rằng một cỗ máy ngu ngốc có thể đoán được, từ khía cạnh "chuyên môn hóa" (var "của Milner "Quy tắc) có hiệu quả như người giải quyết ràng buộc của bạn. Chúng ta có thể hy vọng rằng các loại cấp cao nhất sẽ trở nên khó suy luận hơn, nhưng thông tin loại nội bộ sẽ vẫn khá dễ truyền bá.

Các bước tiếp theo cho Haskell

Chúng ta đang thấy các loại và mức độ phát triển rất giống nhau (và họ đã chia sẻ một đại diện nội bộ trong GHC). Chúng tôi cũng có thể hợp nhất chúng. Sẽ rất vui * :: *nếu chúng ta có thể: chúng ta đã mất âm thanh logic từ lâu, khi chúng ta cho phép chạm đáy, nhưng âm thanh loại thường là một yêu cầu yếu hơn. Chúng ta phải kiểm tra. Nếu chúng ta phải có các cấp độ loại, loại, vv khác nhau, ít nhất chúng ta có thể đảm bảo mọi thứ ở cấp độ trở lên luôn có thể được thăng cấp. Sẽ thật tuyệt vời khi sử dụng lại đa hình mà chúng ta đã có cho các loại, thay vì tái phát minh đa hình ở cấp độ loại.

Chúng ta nên đơn giản hóa và khái quát hóa hệ thống các ràng buộc hiện tại bằng cách cho phép các phương trình không đồng nhấta ~ b trong đó các loại abkhông giống nhau về mặt cú pháp (nhưng có thể được chứng minh bằng nhau). Đó là một kỹ thuật cũ (trong luận án của tôi, thế kỷ trước) khiến cho sự phụ thuộc dễ dàng hơn nhiều để đối phó. Chúng tôi có thể thể hiện các ràng buộc đối với các biểu thức trong GADT và do đó nới lỏng các hạn chế đối với những gì có thể được quảng bá.

Chúng ta nên loại bỏ sự cần thiết cho việc xây dựng đơn lẻ bằng cách giới thiệu một loại hàm phụ thuộc , pi x :: s -> t. Một hàm có kiểu như vậy có thể được áp dụng rõ ràng cho bất kỳ biểu thức kiểu snào sống trong giao điểm của ngôn ngữ loại và thuật ngữ (vì vậy, các biến, hàm tạo, có nhiều hơn để đến sau). Ứng dụng và ứng dụng lambda tương ứng sẽ không bị xóa trong thời gian chạy, vì vậy chúng tôi có thể viết

vReplicate :: pi n :: Nat -> x -> Vec n x
vReplicate Z     x = VNil
vReplicate (S n) x = VCons x (vReplicate n x)

mà không thay thế Natbởi Natty. Miền của picó thể là bất kỳ loại có thể quảng bá nào, vì vậy nếu GADT có thể được quảng bá, chúng ta có thể viết các chuỗi định lượng phụ thuộc (hoặc "kính thiên văn" như de Briuijn gọi chúng)

pi n :: Nat -> pi xs :: Vec n x -> ...

đến bất cứ độ dài nào chúng ta cần.

Điểm của các bước này là loại bỏ sự phức tạp bằng cách làm việc trực tiếp với các công cụ chung hơn, thay vì thực hiện với các công cụ yếu và mã hóa cồng kềnh. Việc mua một phần hiện tại làm cho lợi ích của các loại phụ thuộc của Haskell đắt hơn mức cần thiết.

Quá khó?

Các loại phụ thuộc làm cho rất nhiều người lo lắng. Họ làm tôi lo lắng, nhưng tôi thích lo lắng, hoặc ít nhất tôi thấy khó mà không lo lắng. Nhưng điều đó không giúp ích gì cho việc có một màn sương mù vô minh như vậy xung quanh chủ đề này. Một số điều đó là do thực tế là tất cả chúng ta vẫn còn nhiều điều để học hỏi. Nhưng những người đề xuất các phương pháp ít triệt để hơn đã được biết là gây ra nỗi sợ về các loại phụ thuộc mà không luôn đảm bảo sự thật là hoàn toàn với họ. Tôi sẽ không đặt tên. Những "lỗi đánh máy không thể giải quyết được", "Turing không đầy đủ", "không phân biệt pha", "không xóa kiểu", "bằng chứng ở khắp mọi nơi", v.v., những huyền thoại vẫn tồn tại, mặc dù chúng là rác rưởi.

Đó chắc chắn không phải là trường hợp các chương trình gõ phụ thuộc phải luôn được chứng minh là đúng. Người ta có thể cải thiện vệ sinh cơ bản của các chương trình của một người, thực thi các bất biến bổ sung trong các loại mà không đi đến một đặc điểm kỹ thuật đầy đủ. Các bước nhỏ theo hướng này khá thường xuyên dẫn đến đảm bảo mạnh mẽ hơn nhiều với ít hoặc không có nghĩa vụ chứng minh bổ sung. Không phải sự thật là các chương trình gõ phụ thuộc chắc chắn có đầy đủ bằng chứng, thực sự tôi thường lấy sự hiện diện của bất kỳ bằng chứng nào trong mã của mình làm gợi ý cho câu hỏi định nghĩa của tôi .

Đối với, như với bất kỳ sự gia tăng trong khớp nối, chúng ta trở nên tự do để nói những điều mới lạ cũng như công bằng. Ví dụ: có rất nhiều cách hay để xác định cây tìm kiếm nhị phân, nhưng điều đó không có nghĩa là không có cách nào tốt . Điều quan trọng là không cho rằng những trải nghiệm xấu không thể được cải thiện, ngay cả khi nó làm cho bản ngã thừa nhận nó. Thiết kế các định nghĩa phụ thuộc là một kỹ năng mới cần học hỏi và trở thành lập trình viên Haskell không tự động biến bạn thành một chuyên gia! Và ngay cả khi một số chương trình bị phạm lỗi, tại sao bạn lại từ chối người khác tự do công bằng?

Tại sao vẫn còn phiền với Haskell?

Tôi thực sự thích các loại phụ thuộc, nhưng hầu hết các dự án hack của tôi vẫn ở Haskell. Tại sao? Haskell có các lớp loại. Haskell có các thư viện hữu ích. Haskell có một phương pháp điều trị khả thi (mặc dù không lý tưởng) về lập trình với các hiệu ứng. Haskell có một trình biên dịch sức mạnh công nghiệp. Các ngôn ngữ được gõ phụ thuộc ở giai đoạn sớm hơn trong cộng đồng và cơ sở hạ tầng đang phát triển, nhưng chúng ta sẽ đến đó, với sự thay đổi thế hệ thực sự trong những gì có thể, ví dụ, bằng cách tổng hợp siêu dữ liệu và tổng hợp kiểu dữ liệu. Nhưng bạn chỉ cần nhìn xung quanh những gì mọi người đang làm do các bước của Haskell đối với các loại phụ thuộc để thấy rằng có rất nhiều lợi ích để đạt được bằng cách đẩy thế hệ ngôn ngữ hiện tại về phía trước.


6
Tôi thực sự không quan tâm đến các công cụ DataKinds. Chủ yếu là vì tôi muốn làm một cái gì đó như thế này : fmap read getLine >>= \n -> vReplicate n 0. Như bạn lưu ý, Nattylà một cách xa này. Hơn nữa, vReplicate nên có thể dịch sang một mảng bộ nhớ thực tế, giống như newtype SVector n x = SVector (Data.Vector.Vector x), nơi ncó loại Nat(hoặc tương tự). Có lẽ một điểm trình diễn khác cho một "phô trương phụ thuộc?"
John L

7
Bạn có thể nói những gì bạn có trong đầu cho một điều trị lý tưởng về lập trình với các hiệu ứng?
Steven Shaw

6
Cảm ơn vì bài viết tuyệt vời. Tôi rất muốn xem một số ví dụ về mã được gõ phụ thuộc trong đó một số dữ liệu bắt nguồn bên ngoài chương trình (ví dụ đọc từ tệp), để có cảm giác về việc quảng cáo giá trị cho các loại sẽ như thế nào trong một cài đặt như vậy. Tôi có cảm giác rằng tất cả các ví dụ liên quan đến các vectơ (được thực hiện dưới dạng danh sách) với các kích thước được biết đến tĩnh.
tibbe

4
@pigworker Bạn coi "không phân biệt giai đoạn" là một huyền thoại (những người khác tôi đồng ý là huyền thoại). Nhưng bạn đã không tháo gỡ cái này trong các bài báo và các cuộc nói chuyện mà tôi đã thấy, và trong khi đó, một người khác mà tôi tôn trọng nói với tôi "lý thuyết loại phụ thuộc khác với một trình biên dịch điển hình bởi vì chúng ta không thể tách rời các giai đoạn kiểm tra, biên dịch và thực thi một cách có ý nghĩa. " (xem bài đăng mới nhất của Andrej trong tiểu thuyết 8 2012) Theo kinh nghiệm của tôi "giả mạo", đôi khi chúng ta ít nhất làm mờ sự phân biệt pha mặc dù không cần phải xóa nó. Bạn có thể mở rộng, nếu không ở đây sau đó ở nơi khác, về vấn đề này?
sclv

4
@sclv Công việc của tôi không đặc biệt nhắm vào huyền thoại "không phân biệt giai đoạn", nhưng những người khác thì có. Tôi đề nghị từ chối "Phân biệt giai đoạn trong việc biên soạn Epigram", của James McKinna và Edwin Brady, là một nơi tốt để bắt đầu. Nhưng cũng thấy nhiều công việc cũ hơn về Khai thác chương trình trong Coq. Việc đánh giá các thuật ngữ mở được thực hiện bởi máy đánh chữ hoàn toàn tách biệt với việc thực thi thông qua trích xuất sang ML và rõ ràng là trích xuất loại bỏ các loại và bằng chứng.
thợ lợn

20

John đó là một quan niệm sai lầm phổ biến khác về các loại phụ thuộc: rằng chúng không hoạt động khi dữ liệu chỉ có sẵn trong thời gian chạy. Đây là cách bạn có thể làm ví dụ getLine:

data Some :: (k -> *) -> * where
  Like :: p x -> Some p

fromInt :: Int -> Some Natty
fromInt 0 = Like Zy
fromInt n = case fromInt (n - 1) of
  Like n -> Like (Sy n)

withZeroes :: (forall n. Vec n Int -> IO a) -> IO a
withZeroes k = do
  Like n <- fmap (fromInt . read) getLine
  k (vReplicate n 0)

*Main> withZeroes print
5
VCons 0 (VCons 0 (VCons 0 (VCons 0 (VCons 0 VNil))))

Chỉnh sửa: Hừm, đó được cho là một nhận xét cho câu trả lời của người chăn lợn. Tôi rõ ràng thất bại tại SO.


Câu đầu tiên của bạn có vẻ hơi kỳ lạ; Tôi có thể nói điểm của các loại phụ thuộc là họ làm việc khi dữ liệu chỉ có sẵn tại thời gian chạy. Tuy nhiên, kỹ thuật kiểu CPS này không giống nhau. Giả sử bạn có một chức năng Vec Zy -> IO String. Bạn không thể sử dụng nó với withZeroes, bởi vì loại Zykhông thể hợp nhất với forall n. Có thể bạn có thể giải quyết vấn đề đó trong một hoặc hai trường hợp đặc biệt, nhưng nó nhanh chóng vượt ra khỏi tầm tay.
John L

Chìa khóa khi lấy một giá trị được gõ đơn giản (như Chuỗi từ getLine) và biến nó thành một thứ có loại mạnh hơn (như Natty n ở trên) là bạn phải thuyết phục trình kiểm tra loại mà bạn đang thực hiện kiểm tra động cần thiết. Trong ví dụ của bạn, bạn đang đọc một số tùy ý để forall ncó ý nghĩa. Hạn chế chính xác hơn có thể được thực hiện theo cùng một cách. Bạn có một ví dụ tốt hơn Vec Zy(chương trình vẫn cần xử lý người dùng nhập 5 thay vì 0) không?
ulfnorell

1
Điều tôi muốn nói với câu đầu tiên là tôi thỉnh thoảng gặp những người tin rằng bạn không thể sử dụng các loại phụ thuộc nếu bạn nhận được dữ liệu của mình bằng cách tương tác với thế giới bên ngoài. Quan điểm của tôi là điều duy nhất bạn phải làm là viết một trình phân tích cú pháp phụ thuộc, thường là đơn giản.
ulfnorell

1
ulfnorell: Xin lỗi, tôi đã không rõ ràng. Giả sử bạn có một chức năng sẽ hoạt động cùng với một chức năng Vec Zy -> IO Stringkhác Vec n -> IO Stringvà bạn chỉ muốn sử dụng chức năng đầu tiên nếu loại phù hợp. Vâng, điều đó là có thể, nhưng các cơ chế cho phép nó rất khó hiểu. Và đây là logic rất đơn giản; nếu bạn có logic phức tạp hơn thì nó tệ hơn. Ngoài ra, bạn có thể cần phải viết lại rất nhiều mã trong CPS. Và bạn vẫn không có biểu thức cấp loại phụ thuộc vào một thuật ngữ ở cấp giá trị
John L

Ah, tôi thấy những gì bạn đang nói. Đây là những gì Natty dành cho, như trong vReplicate nơi chúng tôi làm những việc khác nhau tùy thuộc vào n. Quả thực điều này có thể có được một chút vụng về. Một thay thế cho kiểu CPS là làm việc với các tồn tại đóng gói : zeroes :: IO (Some (Flip Vec Int)).
ulfnorell

19

pigworker đưa ra một cuộc thảo luận tuyệt vời về lý do tại sao chúng ta nên hướng đến các loại phụ thuộc: (a) chúng tuyệt vời; (b) họ thực sự sẽ đơn giản hóa rất nhiều những gì Haskell đã làm.

Đối với "tại sao không?" Câu hỏi, có một vài điểm tôi nghĩ. Điểm đầu tiên là trong khi khái niệm cơ bản đằng sau các loại phụ thuộc là dễ dàng (cho phép các loại phụ thuộc vào các giá trị), thì sự phân nhánh của khái niệm cơ bản đó vừa tinh tế vừa sâu sắc. Ví dụ, sự khác biệt giữa các giá trị và loại vẫn còn sống và tốt; nhưng thảo luận về sự khác biệt giữa chúng trở nên xanhiều sắc thái hơn so với trong Hindley - Milner hoặc System F. Trong một chừng mực nào đó, điều này là do thực tế là các loại phụ thuộc rất khó về cơ bản (ví dụ, logic thứ nhất là không thể giải quyết được). Nhưng tôi nghĩ vấn đề lớn hơn thực sự là chúng ta thiếu vốn từ vựng tốt để nắm bắt và giải thích những gì đang diễn ra. Khi ngày càng có nhiều người tìm hiểu về các loại phụ thuộc, chúng tôi sẽ phát triển vốn từ vựng tốt hơn và do đó mọi thứ sẽ trở nên dễ hiểu hơn, ngay cả khi các vấn đề tiềm ẩn vẫn còn khó khăn.

Điểm thứ hai liên quan đến thực tế là Haskell đang phát triểnhướng tới các loại phụ thuộc. Bởi vì chúng tôi đang đạt được tiến bộ gia tăng đối với mục tiêu đó, nhưng không thực sự đạt được mục tiêu đó, chúng tôi bị mắc kẹt với một ngôn ngữ có các bản vá tăng dần trên đầu các bản vá tăng dần. Điều tương tự đã xảy ra trong các ngôn ngữ khác khi những ý tưởng mới trở nên phổ biến. Java đã không sử dụng để có đa hình (tham số); và cuối cùng khi họ thêm nó, rõ ràng đó là một sự cải tiến gia tăng với một số rò rỉ trừu tượng và sức mạnh tê liệt. Hóa ra, pha trộn phân nhóm và đa hình vốn đã khó; nhưng đó không phải là lý do tại sao Java Generics hoạt động theo cách họ làm. Chúng hoạt động theo cách chúng làm vì ràng buộc là một cải tiến gia tăng so với các phiên bản Java cũ hơn. Ditto, để quay trở lại vào ngày mà OOP được phát minh và mọi người bắt đầu viết "khách quan" C. Quan điểm của tôi trong tất cả những điều này là, việc thêm các loại phụ thuộc thực sự vào Haskell sẽ đòi hỏi một số lượng nhất định để chuyển đổi và cấu trúc lại ngôn ngữ --- nếu chúng ta sẽ làm đúng. Nhưng thực sự rất khó để cam kết với một cuộc đại tu như vậy, trong khi tiến bộ gia tăng mà chúng tôi đang thực hiện có vẻ rẻ hơn trong thời gian ngắn. Thực sự, không có nhiều người hack GHC, nhưng có một số lượng lớn mã di sản để duy trì sự sống. Đây là một phần lý do tại sao có rất nhiều ngôn ngữ spinoff như DDC, Cayenne, Idris, v.v. C ++ khởi đầu dưới chiêu bài là một superset nghiêm ngặt của C. Thêm các mô hình mới luôn đòi hỏi phải xác định ngôn ngữ một lần nữa, hoặc nếu không thì kết thúc bằng một mớ hỗn độn phức tạp. Quan điểm của tôi trong tất cả những điều này là, việc thêm các loại phụ thuộc thực sự vào Haskell sẽ đòi hỏi một số lượng nhất định để chuyển đổi và cấu trúc lại ngôn ngữ --- nếu chúng ta sẽ làm đúng. Nhưng thực sự rất khó để cam kết với một cuộc đại tu như vậy, trong khi tiến bộ gia tăng mà chúng tôi đang thực hiện có vẻ rẻ hơn trong thời gian ngắn. Thực sự, không có nhiều người hack GHC, nhưng có một số lượng lớn mã di sản để duy trì sự sống. Đây là một phần lý do tại sao có rất nhiều ngôn ngữ spinoff như DDC, Cayenne, Idris, v.v. C ++ khởi đầu dưới chiêu bài là một superset nghiêm ngặt của C. Thêm các mô hình mới luôn đòi hỏi phải xác định ngôn ngữ một lần nữa, hoặc nếu không thì kết thúc bằng một mớ hỗn độn phức tạp. Quan điểm của tôi trong tất cả những điều này là, việc thêm các loại phụ thuộc thực sự vào Haskell sẽ đòi hỏi một số lượng nhất định để chuyển đổi và cấu trúc lại ngôn ngữ --- nếu chúng ta sẽ làm đúng. Nhưng thực sự rất khó để cam kết với một cuộc đại tu như vậy, trong khi tiến bộ gia tăng mà chúng tôi đang thực hiện có vẻ rẻ hơn trong thời gian ngắn. Thực sự, không có nhiều người hack GHC, nhưng có một số lượng lớn mã di sản để duy trì sự sống. Đây là một phần lý do tại sao có rất nhiều ngôn ngữ spinoff như DDC, Cayenne, Idris, v.v. hoặc kết thúc với một số lộn xộn phức tạp. Quan điểm của tôi trong tất cả những điều này là, việc thêm các loại phụ thuộc thực sự vào Haskell sẽ đòi hỏi một số lượng nhất định để chuyển đổi và cấu trúc lại ngôn ngữ --- nếu chúng ta sẽ làm đúng. Nhưng thực sự rất khó để cam kết với một cuộc đại tu như vậy, trong khi tiến bộ gia tăng mà chúng tôi đang thực hiện có vẻ rẻ hơn trong thời gian ngắn. Thực sự, không có nhiều người hack GHC, nhưng có một số lượng lớn mã di sản để duy trì sự sống. Đây là một phần lý do tại sao có rất nhiều ngôn ngữ spinoff như DDC, Cayenne, Idris, v.v. hoặc kết thúc với một số lộn xộn phức tạp. Quan điểm của tôi trong tất cả những điều này là, việc thêm các loại phụ thuộc thực sự vào Haskell sẽ đòi hỏi một số lượng nhất định để chuyển đổi và cấu trúc lại ngôn ngữ --- nếu chúng ta sẽ làm đúng. Nhưng thực sự rất khó để cam kết với một cuộc đại tu như vậy, trong khi tiến bộ gia tăng mà chúng tôi đang thực hiện có vẻ rẻ hơn trong thời gian ngắn. Thực sự, không có nhiều người hack GHC, nhưng có một số lượng lớn mã di sản để duy trì sự sống. Đây là một phần lý do tại sao có rất nhiều ngôn ngữ spinoff như DDC, Cayenne, Idris, v.v. thực sự rất khó để cam kết với loại đại tu đó, trong khi tiến độ gia tăng mà chúng tôi đang thực hiện có vẻ rẻ hơn trong thời gian ngắn. Thực sự, không có nhiều người hack GHC, nhưng có một số lượng lớn mã di sản để duy trì sự sống. Đây là một phần lý do tại sao có rất nhiều ngôn ngữ spinoff như DDC, Cayenne, Idris, v.v. thực sự rất khó để cam kết với loại đại tu đó, trong khi tiến độ gia tăng mà chúng tôi đang thực hiện có vẻ rẻ hơn trong thời gian ngắn. Thực sự, không có nhiều người hack GHC, nhưng có một số lượng lớn mã di sản để duy trì sự sống. Đây là một phần lý do tại sao có rất nhiều ngôn ngữ spinoff như DDC, Cayenne, Idris, v.v.

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.