Điều gì chính xác làm cho hệ thống loại Haskell rất được tôn kính (so với nói, Java)?


204

Tôi đang bắt đầu học Haskell . Tôi rất mới với nó, và tôi chỉ đọc qua một vài cuốn sách trực tuyến để tìm hiểu về các cấu trúc cơ bản của nó.

Một trong những 'meme' mà mọi người quen thuộc với nó thường nói đến, đó là toàn bộ "nếu nó biên dịch, nó sẽ hoạt động *" - điều mà tôi nghĩ có liên quan đến sức mạnh của hệ thống loại.

Tôi đang cố gắng để hiểu tại sao chính xác Haskell tốt hơn các ngôn ngữ gõ tĩnh khác trong vấn đề này.

Nói cách khác, tôi giả sử trong Java, bạn có thể làm điều gì đó ghê tởm như chôn vùi ArrayList<String>()để chứa thứ gì đó thực sự nên có ArrayList<Animal>(). Điều khủng khiếp ở đây là stringchứa của bạn elephant, giraffe, v.v. và nếu có ai đó đưa vào Mercedes- trình biên dịch của bạn sẽ không giúp bạn.

Nếu tôi đã làm ArrayList<Animal>()sau đó, vào một thời điểm sau đó, nếu tôi quyết định chương trình của tôi không thực sự là về động vật, thì đó là về phương tiện, sau đó tôi có thể thay đổi, một chức năng sản xuất ArrayList<Animal>để sản xuất ArrayList<Vehicle>và IDE của tôi sẽ cho tôi biết mọi nơi ở đó là một sự phá vỡ biên dịch.

Giả định của tôi là đây là ý nghĩa của mọi người đối với một hệ thống loại mạnh , nhưng đối với tôi thì không rõ tại sao Haskell lại tốt hơn. Nói cách khác, bạn có thể viết Java tốt hay xấu, tôi giả sử bạn có thể làm tương tự trong Haskell (tức là nhét mọi thứ vào chuỗi / int thực sự phải là kiểu dữ liệu hạng nhất).

Tôi nghi ngờ tôi đang thiếu một cái gì đó quan trọng / cơ bản.
Tôi sẽ rất vui khi được hiển thị lỗi theo cách của tôi!


31
Tôi sẽ cho mọi người hiểu biết nhiều hơn tôi viết câu trả lời thực sự, nhưng ý chính của nó là: các ngôn ngữ được gõ tĩnh như C # có một hệ thống loại cố gắng giúp bạn viết mã có thể phòng thủ được ; loại hệ thống như nỗ lực của Haskell để giúp bạn viết mã chính xác (nghĩa là có thể chứng minh). Nguyên tắc cơ bản trong công việc là di chuyển những thứ có thể được kiểm tra vào giai đoạn biên dịch; Haskell kiểm tra nhiều thứ hơn vào thời gian biên dịch.
Robert Harvey

8
Tôi không biết nhiều về Haskell, nhưng tôi có thể nói về Java. Mặc dù có vẻ như được đánh máy mạnh mẽ, nó vẫn cho phép bạn làm những việc "ghê tởm" như bạn đã nói. Đối với hầu hết mọi bảo đảm mà Java đưa ra liên quan đến hệ thống kiểu của nó, có một cách xung quanh nó.

12
Tôi không biết tại sao tất cả các câu trả lời Maybechỉ đề cập đến cuối cùng. Nếu tôi phải chọn chỉ một điều mà các ngôn ngữ phổ biến hơn nên mượn từ Haskell, thì đây sẽ là nó. Đó là một ý tưởng rất đơn giản (vì vậy không thú vị theo quan điểm lý thuyết), nhưng điều này một mình sẽ làm cho công việc của chúng tôi dễ dàng hơn nhiều.
Paul

1
Sẽ có câu trả lời tuyệt vời ở đây, nhưng trong nỗ lực hỗ trợ, nghiên cứu chữ ký loại. Chúng cho phép con người và các chương trình suy luận về các chương trình theo cách sẽ minh họa cách Java ở giữa các loại trung gian.
Michael Easter

6
Để công bằng, tôi phải chỉ ra rằng "toàn bộ nếu nó biên dịch, nó sẽ hoạt động được" là một khẩu hiệu không phải là một tuyên bố theo nghĩa đen. Vâng, chúng tôi lập trình viên Haskell biết rằng việc vượt qua trình kiểm tra kiểu sẽ mang lại cơ hội đúng đắn, cho một số khái niệm hạn chế về tính đúng đắn, nhưng chắc chắn đó không phải là một tuyên bố "đúng" theo nghĩa đen và phổ quát!
Tom Ellis

Câu trả lời:


230

Đây là danh sách các tính năng hệ thống loại không có thứ tự có sẵn trong Haskell và không có sẵn hoặc kém đẹp hơn trong Java (theo hiểu biết của tôi, vốn được thừa nhận là Java yếu)

  • An toàn . Các loại của Haskell có thuộc tính "an toàn loại" khá tốt. Điều này khá cụ thể, nhưng về cơ bản có nghĩa là các giá trị ở một số loại không thể chuyển đổi một cách bừa bãi thành một loại khác. Điều này đôi khi xảy ra mâu thuẫn với khả năng biến đổi (xem giới hạn giá trị của OCaml )
  • Các kiểu dữ liệu đại số . Các loại trong Haskell về cơ bản có cấu trúc tương tự như toán học trung học. Điều này là cực kỳ đơn giản và nhất quán, nhưng, hóa ra, mạnh mẽ như bạn có thể muốn. Nó chỉ đơn giản là một nền tảng tuyệt vời cho một hệ thống loại.
    • Lập trình kiểu dữ liệu chung . Điều này không giống như các loại chung chung (xem khái quát hóa ). Thay vào đó, do sự đơn giản của cấu trúc kiểu như đã lưu ý trước đây, việc viết mã hoạt động chung trên cấu trúc đó tương đối dễ dàng. Sau này tôi nói về việc một cái gì đó như Equality có thể được lấy tự động cho loại do người dùng xác định bởi trình biên dịch Haskell. Về cơ bản, cách thức thực hiện điều này là đi qua cấu trúc đơn giản, phổ biến bên dưới bất kỳ loại do người dùng xác định và khớp với nó giữa các giá trị, một dạng bình đẳng cấu trúc rất tự nhiên.
  • Các loại đệ quy lẫn nhau . Đây chỉ là một thành phần thiết yếu của việc viết các loại không tầm thường.
    • Các loại lồng nhau . Điều này cho phép bạn xác định các kiểu đệ quy trên các biến lặp lại ở các loại khác nhau. Ví dụ, một loại cây cân bằng là data Bt a = Here a | There (Bt (a, a)). Hãy suy nghĩ cẩn thận về các giá trị hợp lệ Bt avà chú ý cách thức hoạt động của loại đó. Thật là khó khăn!
  • Khái quát hóa . Điều này gần như quá ngớ ngẩn khi không có trong một hệ thống loại (ahem, nhìn vào bạn, Go). Điều quan trọng là phải có các khái niệm về biến loại và khả năng nói về mã độc lập với sự lựa chọn của biến đó. Hindley Milner là một hệ thống kiểu đó có nguồn gốc từ hệ thống kiểu hệ thống F. Haskell là một xây dựng HM đánh máy và hệ thống F chủ yếu là lò sưởi của tổng quát. Điều tôi muốn nói là Haskell có một câu chuyện khái quát rất hay .
  • Các loại trừu tượng . Câu chuyện của Haskell ở đây không tuyệt vời nhưng cũng không phải là không tồn tại. Có thể viết các loại có giao diện chung nhưng thực hiện riêng. Điều này cho phép chúng ta vừa thừa nhận các thay đổi đối với mã triển khai sau đó và quan trọng là vì đó là nền tảng của tất cả các hoạt động trong Haskell, hãy viết các loại "ma thuật" có giao diện được xác định rõ như IO. Thành thật mà nói, Java có một câu chuyện trừu tượng đẹp hơn, nhưng tôi không nghĩ cho đến khi Giao diện trở nên phổ biến hơn thì điều đó thực sự đúng.
  • Tham số . Giá trị Haskell không có bất kỳ hoạt động phổ quát. Java vi phạm điều này với những thứ như bình đẳng tham chiếu và băm và thậm chí còn nghiêm túc hơn với các ép buộc. Điều này có nghĩa là bạn có được các định lý miễn phí về các loại cho phép bạn biết ý nghĩa của một hoạt động hoặc giá trị ở mức độ đáng chú ý hoàn toàn từ loại của nó --- một số loại nhất định chỉ có thể có một số lượng rất nhỏ cư dân.
  • Các loại cao hơn hiển thị tất cả các loại khi mã hóa những thứ phức tạp hơn. Functor / Applicative / Monad, Có thể gập lại / Traversable, toàn bộ mtlhệ thống gõ hiệu ứng, các điểm cố định functor tổng quát. Danh sách đi và về. Có rất nhiều điều được thể hiện tốt nhất ở các loại cao hơn và tương đối ít hệ thống loại thậm chí cho phép người dùng nói về những điều này.
  • Loại lớp . Nếu bạn nghĩ về các hệ thống kiểu như logics thì rất hữu ích, thì bạn thường được yêu cầu chứng minh mọi thứ. Trong nhiều trường hợp, đây thực chất là nhiễu đường truyền: có thể chỉ có một câu trả lời đúng và thật lãng phí thời gian và công sức để lập trình viên nêu ra điều này. Máy đánh chữ là một cách để Haskell tạo ra bằng chứng cho bạn. Nói một cách cụ thể hơn, điều này cho phép bạn giải quyết các "hệ phương trình kiểu" đơn giản như "Chúng ta đang có ý định (+)gì với nhau? Ồ Integer, ok! Hãy nhập mã ngay bây giờ!". Tại các hệ thống phức tạp hơn, bạn có thể thiết lập các ràng buộc thú vị hơn.
    • Tính toán ràng buộc . Các ràng buộc trong Haskell, đó là cơ chế để tiếp cận hệ thống prologlass typologlass được gõ theo cấu trúc. Điều này mang lại một hình thức phân nhóm rất đơn giản cho phép bạn tập hợp các ràng buộc phức tạp từ các mối quan hệ đơn giản hơn. Toàn bộ mtlthư viện dựa trên ý tưởng này.
    • Xuất phát . Để điều khiển tính mạnh mẽ của hệ thống kiểu chữ, cần phải viết rất nhiều mã thường để mô tả các ràng buộc mà các kiểu do người dùng xác định phải khởi tạo. Do cấu trúc rất bình thường của các loại Haskell, thường có thể yêu cầu trình biên dịch thực hiện bản tóm tắt này cho bạn.
    • Loại prolog lớp . Trình giải quyết lớp loại Haskell, hệ thống tạo ra các "bằng chứng" mà tôi đã đề cập trước đó về cơ bản là một dạng Prolog bị tê liệt với các thuộc tính ngữ nghĩa đẹp hơn. Điều này có nghĩa là bạn có thể mã hóa những thứ thực sự có lông trong kiểu prolog và hy vọng chúng sẽ được xử lý tất cả tại thời gian biên dịch. Một ví dụ điển hình có thể là giải quyết cho một bằng chứng cho thấy hai danh sách không đồng nhất là tương đương nếu bạn quên thứ tự "các bộ" không đồng nhất tương đương.
    • Các lớp loại đa tham số và phụ thuộc chức năng . Đây chỉ là những tinh chỉnh hữu ích ồ ạt cho prolog typeclass cơ sở. Nếu bạn biết Prolog, bạn có thể tưởng tượng sức mạnh biểu cảm tăng lên bao nhiêu khi bạn có thể viết các biến vị ngữ có nhiều hơn một biến.
  • Suy luận khá tốt . Các ngôn ngữ dựa trên hệ thống loại Hindley Milner có suy luận khá tốt. Bản thân HM có suy luận hoàn chỉnh có nghĩa là bạn không bao giờ cần phải viết biến kiểu. Haskell 98, hình thức đơn giản nhất của Haskell, đã ném nó ra trong một số trường hợp rất hiếm. Nói chung, Haskell hiện đại đã là một thử nghiệm trong việc giảm dần không gian suy luận hoàn toàn đồng thời tăng thêm sức mạnh cho HM và nhìn thấy khi người dùng phàn nàn. Mọi người rất hiếm khi phàn nàn về suy luận của Haskell là khá tốt.
  • Rất, rất, rất yếu chỉ phân nhóm . Tôi đã đề cập trước đó rằng hệ thống ràng buộc từ prologlass typologlass có một khái niệm về phân nhóm cấu trúc. Đó là hình thức duy nhất của phân nhóm trong Haskell . Subtyping là khủng khiếp cho lý luận và suy luận. Nó làm cho mỗi vấn đề trở nên khó khăn hơn đáng kể (một hệ thống bất bình đẳng thay vì một hệ thống bình đẳng). Điều này cũng rất dễ hiểu lầm (Có phải phân lớp giống như phân nhóm không? Tất nhiên là không! Nhưng mọi người rất hay nhầm lẫn điều đó và nhiều ngôn ngữ hỗ trợ cho sự nhầm lẫn đó!
    • Lưu ý gần đây (đầu năm 2017) Steven Dolan đã xuất bản luận án của mình về MLsub , một biến thể của suy luận kiểu ML và Hindley-Milner có một câu chuyện phân nhóm rất hay ( xem thêm ). Điều này không làm mất đi những gì tôi đã viết ở trên, hầu hết các hệ thống phân nhóm đều bị hỏng và có suy luận xấu, nhưng điều đó cho thấy rằng chúng ta vừa mới phát hiện ra một số cách hứa hẹn để hoàn thành suy luận và phân nhóm chơi cùng nhau. Bây giờ, để hoàn toàn rõ ràng, các khái niệm về phân nhóm của Java không có cách nào có thể tận dụng các thuật toán và hệ thống của Dolan. Nó đòi hỏi phải suy nghĩ lại về ý nghĩa của việc phân nhóm.
  • Các loại thứ hạng cao hơn . Tôi đã nói về khái quát hóa sớm hơn, nhưng không chỉ là khái quát hóa, thật hữu ích khi có thể nói về các loại có các biến tổng quát trong chúng . Ví dụ, ánh xạ giữa các cấu trúc bậc cao không rõ ràng (xem tham số ) với những cấu trúc "chứa" có kiểu như thế nào (forall a. f a -> g a). Trong HM thẳng, bạn có thể viết một hàm ở loại này, nhưng với các loại có thứ hạng cao hơn, bạn yêu cầu một hàm như một đối số như vậy : mapFree :: (forall a . f a -> g a) -> Free f -> Free g. Lưu ý rằng abiến chỉ bị ràng buộc trong phạm vi đối số. Điều này có nghĩa là bộ khử của hàm mapFreesẽ quyết định cái gì ađược khởi tạo khi họ sử dụng nó chứ không phải người dùng mapFree.
  • Các loại tồn tại . Trong khi các loại thứ hạng cao hơn cho phép chúng ta nói về định lượng phổ quát, các loại hiện sinh cho phép chúng ta nói về định lượng hiện sinh: ý tưởng rằng chỉ tồn tại một số loại chưa biết thỏa mãn một số phương trình. Điều này kết thúc là hữu ích và để tiếp tục lâu hơn về nó sẽ mất một thời gian dài.
  • Loại gia đình . Đôi khi các cơ chế đánh máy không thuận tiện vì chúng ta không luôn nghĩ về Prolog. Loại gia đình cho chúng tôi viết mối quan hệ chức năng thẳng giữa các loại.
    • Gia đình kiểu kín . Các gia đình kiểu mặc định mở rất khó chịu vì điều đó có nghĩa là trong khi bạn có thể gia hạn họ bất cứ lúc nào bạn không thể "đảo ngược" họ với bất kỳ hy vọng thành công nào. Điều này là do bạn không thể chứng minh được sự tiêm nhiễm , nhưng với những gia đình kiểu kín bạn có thể.
  • Loại chỉ mục và loại khuyến mãi . Tôi đang trở nên thực sự kỳ lạ vào thời điểm này, nhưng đôi khi chúng có những ứng dụng thực tế. Nếu bạn muốn viết một loại tay cầm mở hoặc đóng thì bạn có thể làm điều đó rất độc đáo. Lưu ý trong đoạn mã sau đây Statelà một loại đại số rất đơn giản, cũng có các giá trị của nó được thăng cấp thành cấp độ loại. Sau đó, sau đó, chúng ta có thể nói về các hàm tạo kiểu Handlenhư lấy các đối số ở các loại cụ thể như State. Thật khó hiểu khi hiểu tất cả các chi tiết, nhưng cũng rất đúng.

    data State = Open | Closed
    
    data Handle :: State -> * -> * where
      OpenHandle :: {- something -} -> Handle Open a
      ClosedHandle :: {- something -} -> Handle Closed a
  • Đại diện loại thời gian chạy mà làm việc . Java nổi tiếng là có kiểu xóa và có tính năng mưa trên các cuộc diễu hành của một số người. Loại xóa cách đúng đắn để đi, tuy nhiên, như thể bạn có một chức năng getRepr :: a -> TypeReprthì bạn ít nhất là vi phạm tham số. Điều tồi tệ hơn là nếu đó là một chức năng do người dùng tạo ra được sử dụng để kích hoạt các ép buộc không an toàn trong thời gian chạy ... thì bạn đã có một mối lo ngại lớn về an toàn . TypeableHệ thống của Haskell cho phép tạo ra một két sắt coerce :: (Typeable a, Typeable b) => a -> Maybe b. Hệ thống này phụ thuộc vào Typeableviệc được triển khai trong trình biên dịch (chứ không phải người dùng) và cũng không thể được cung cấp ngữ nghĩa tốt đẹp như vậy nếu không có cơ chế đánh máy của Haskell và các luật mà nó được đảm bảo tuân theo.

Tuy nhiên, nhiều hơn những giá trị của hệ thống loại Haskell cũng liên quan đến cách các loại mô tả ngôn ngữ. Dưới đây là một vài tính năng của Haskell có giá trị ổ đĩa thông qua hệ thống loại.

  • Độ tinh khiết . Haskell cho phép không có tác dụng phụ cho một định nghĩa rất, rất, rất rộng về "tác dụng phụ". Điều này buộc bạn phải đưa thêm thông tin vào các loại vì các loại chi phối đầu vào và đầu ra và không có tác dụng phụ, mọi thứ phải được tính trong đầu vào và đầu ra.
    • IO . Sau đó, Haskell cần một cách để nói về các tác dụng phụ. Vì bất kỳ chương trình thực tế nào cũng phải bao gồm một số giáo trình, do đó, sự kết hợp của các kiểu chữ, loại cao hơn và các loại trừu tượng đã nảy sinh khái niệm sử dụng một loại đặc biệt, siêu đặc biệt được gọi IO ađể đại diện tính toán tác dụng phụ dẫn đến giá trị của loại a. Đây là nền tảng của một hệ thống hiệu ứng rất đẹp được nhúng bên trong một ngôn ngữ thuần túy.
  • Thiếunull . Mọi người đều biết đó nulllà sai lầm tỷ đô của các ngôn ngữ lập trình hiện đại. Các loại đại số, đặc biệt là khả năng nối thêm trạng thái "không tồn tại" vào các loại bạn có bằng cách chuyển đổi một loại Athành loại Maybe A, giảm thiểu hoàn toàn vấn đề null.
  • Đệ quy đa hình . Điều này cho phép bạn xác định các hàm đệ quy tổng quát hóa các biến loại mặc dù sử dụng chúng ở các loại khác nhau trong mỗi lệnh gọi đệ quy trong tổng quát hóa riêng của chúng. Điều này rất khó để nói, nhưng đặc biệt hữu ích để nói về các loại lồng nhau. Nhìn lại Bt akiểu từ trước và thử viết một hàm để tính kích thước của nó : size :: Bt a -> Int. Nó sẽ trông giống như size (Here a) = 1size (There bt) = 2 * size bt. Hoạt động không quá phức tạp, nhưng lưu ý rằng lệnh gọi đệ quy sizetrong phương trình cuối cùng xảy ra ở một loại khác , tuy nhiên định nghĩa tổng thể có một loại tổng quát đẹp size :: Bt a -> Int. Lưu ý rằng đây là một tính năng phá vỡ toàn bộ suy luận, nhưng nếu bạn cung cấp chữ ký loại thì Haskell sẽ cho phép nó.

Tôi có thể tiếp tục, nhưng danh sách này phải giúp bạn bắt đầu và sau đó một số.


7
Null không phải là một "sai lầm" tỷ đô. Có những trường hợp không thể xác minh tĩnh rằng con trỏ sẽ không bị hủy đăng ký trước khi bất kỳ ý nghĩa nào có thể tồn tại ; có một cái bẫy cố định trong trường hợp như vậy thường tốt hơn là yêu cầu con trỏ ban đầu xác định một đối tượng vô nghĩa. Tôi nghĩ rằng sai lầm lớn nhất liên quan đến null là có các triển khai, được đưa ra char *p = NULL;, sẽ mắc bẫy *p=1234, nhưng sẽ không mắc bẫy char *q = p+5678;cũng không*q = 1234;
supercat

37
Đó chỉ là trích dẫn thẳng từ Tony Hoare: en.wikipedia.org/wiki/Tony_Hoare#Apology_and_retraction . Mặc dù tôi chắc chắn có những lúc nullcần thiết trong số học con trỏ, thay vào đó tôi giải thích rằng để nói rằng số học con trỏ là một nơi tồi tệ để lưu trữ ngữ nghĩa của ngôn ngữ của bạn không phải là null vẫn không phải là một sai lầm.
J. Abrahamson

18
@supercat, bạn thực sự có thể viết một ngôn ngữ mà không null. Có cho phép hay không là một sự lựa chọn.
Paul Draper

6
@supercat - Vấn đề đó cũng tồn tại ở Haskell, nhưng ở một dạng khác. Haskell thường lười biếng và bất biến, và do đó cho phép bạn viết p = undefinedmiễn plà không được đánh giá. Hữu ích hơn, bạn có thể đưa undefinedvào một số loại tham chiếu có thể thay đổi, một lần nữa miễn là bạn không đánh giá nó. Thách thức nghiêm trọng hơn là với các tính toán lười biếng có thể không chấm dứt, điều này tất nhiên là không thể giải quyết được. Sự khác biệt chính là đây đều là những lỗi lập trình rõ ràng và không bao giờ được sử dụng để diễn đạt logic thông thường.
Christian Conkle

6
@supercat Haskell hoàn toàn thiếu ngữ nghĩa tham chiếu (đây là khái niệm về tính minh bạch tham chiếu , đòi hỏi mọi thứ được bảo tồn bằng cách thay thế các tham chiếu bằng tham chiếu của chúng). Vì vậy, tôi nghĩ rằng câu hỏi của bạn là không đúng.
J. Abrahamson

78
  • Kiểu suy luận đầy đủ. Bạn thực sự có thể sử dụng các loại phức tạp có mặt khắp nơi mà không cảm thấy như, "Holy crap, tất cả những gì tôi từng làm là viết chữ ký loại."
  • Các loại là đại số đầy đủ , làm cho nó rất dễ dàng để thể hiện một số ý tưởng phức tạp.
  • Haskell có các lớp loại, giống như các giao diện, ngoại trừ bạn không phải đặt tất cả các triển khai cho một loại ở cùng một nơi. Bạn có thể tạo triển khai các lớp loại của riêng mình cho các loại bên thứ ba hiện có mà không cần truy cập vào nguồn của chúng.
  • Các hàm bậc cao và đệ quy có xu hướng đưa nhiều chức năng hơn vào mục đích của trình kiểm tra loại. Lấy bộ lọc , ví dụ. Trong một ngôn ngữ bắt buộc, bạn có thể viết một forvòng lặp để thực hiện cùng chức năng, nhưng bạn sẽ không có cùng một loại đảm bảo tĩnh, bởi vì một forvòng lặp không có khái niệm về kiểu trả về.
  • Thiếu các kiểu con đơn giản hóa rất nhiều đa hình tham số.
  • Các loại (loại loại) cao hơn tương đối dễ dàng để chỉ định và sử dụng trong Haskell, cho phép bạn tạo các khái niệm trừu tượng xung quanh các loại hoàn toàn không thể hiểu được trong Java.

7
Câu trả lời hay - bạn có thể cho tôi một ví dụ đơn giản về loại tốt hơn, nghĩ rằng điều đó sẽ giúp tôi hiểu tại sao điều đó không thể làm được trong java.
phatmanace

3
Có một số ví dụ tốt ở đây .
Karl Bielefeldt

3
Kết hợp mẫu cũng thực sự quan trọng, nó có nghĩa là bạn có thể sử dụng loại đối tượng để đưa ra quyết định siêu dễ dàng.
Benjamin Gruenbaum

2
@BenjaminGruenbaum Tôi không nghĩ rằng tôi gọi đó là một tính năng hệ thống loại.
Doval

3
Mặc dù ADT và HKT chắc chắn là một phần của câu trả lời, tôi nghi ngờ bất kỳ ai hỏi câu hỏi này sẽ biết lý do tại sao chúng hữu ích, tôi đề nghị cả hai phần cần được mở rộng để giải thích điều này
jk.

62
a :: Integer
b :: Maybe Integer
c :: IO Integer
d :: Either String Integer

Trong Haskell: một số nguyên, một số nguyên có thể là null, một số nguyên có giá trị đến từ thế giới bên ngoài và một số nguyên có thể là một chuỗi thay vào đó, đều là các loại khác nhau - và trình biên dịch sẽ thực thi điều này . Bạn không thể biên dịch chương trình Haskell mà không tôn trọng những khác biệt này.

(Tuy nhiên, bạn có thể bỏ qua các khai báo kiểu. Trong hầu hết các trường hợp, trình biên dịch có thể xác định loại chung nhất cho các biến của bạn, điều này sẽ dẫn đến việc biên dịch thành công. Điều đó có gọn gàng không?)


11
+1 trong khi câu trả lời này chưa đầy đủ Tôi nghĩ rằng nó tốt hơn nhiều ở cấp độ của câu hỏi
jk.

1
+1 Mặc dù sẽ giúp giải thích rằng các ngôn ngữ khác cũng có Maybe(ví dụ như Java Optionalvà Scala Option), nhưng trong các ngôn ngữ đó, đó là một giải pháp nửa vời, vì bạn luôn có thể gán nullcho một biến loại đó và khiến chương trình của bạn phát nổ khi chạy thời gian. Điều này không thể xảy ra với Haskell [1], vì không có giá trị null , vì vậy bạn chỉ đơn giản là không thể gian lận. ([1]: trên thực tế, bạn có thể tạo ra một lỗi tương tự với NullPulumException bằng cách sử dụng các hàm một phần như fromJustkhi bạn có một Nothing, nhưng các chức năng đó có thể bị nhăn mặt).
Andres F.

2
"một số nguyên có giá trị đến từ thế giới bên ngoài" - sẽ không IO Integergần với 'chương trình con hơn, khi được thực thi, sẽ cho số nguyên'? Là một) trong main = c >> cgiá trị trả về bằng cách đầu tiên ccó thể khác nhau sau đó bằng thứ hai ctrong khi asẽ có giá trị như nhau bất kể vị thế của mình (miễn là chúng tôi đang trong phạm vi đơn) b) có nhiều loại biểu thị giá trị từ thế giới bên ngoài để thực thi sanatisation của nó (nghĩa là không đặt chúng trực tiếp nhưng trước tiên hãy kiểm tra xem đầu vào từ người dùng có đúng / không độc hại).
Maciej Piechotka

4
Maciej, điều đó sẽ chính xác hơn. Tôi đã phấn đấu cho sự đơn giản.
WolfeFan

30

Rất nhiều người đã liệt kê những điều tốt về Haskell. Nhưng để trả lời cho câu hỏi cụ thể của bạn "tại sao hệ thống loại làm cho các chương trình chính xác hơn?", Tôi nghi ngờ câu trả lời là "đa hình tham số".

Hãy xem xét chức năng Haskell sau:

foobar :: x -> y -> y

nghĩa đen chỉ có một cách có thể để thực hiện chức năng này. Chỉ bằng chữ ký loại, tôi có thể nói chính xác chức năng này làm gì, bởi vì chỉ có một điều có thểcó thể làm. [OK, không hẳn, nhưng gần như vậy!]

Dừng lại và suy nghĩ về điều đó một lúc. Đó thực sự là một vấn đề lớn! Nó có nghĩa là nếu tôi viết một hàm có chữ ký này, nó thực sự không thể cho các chức năng để làm bất cứ điều gì khác so với những gì tôi mong đợi. (Tất nhiên, chữ ký loại vẫn có thể sai, tất nhiên. Không có ngôn ngữ lập trình nào có thể ngăn chặn tất cả các lỗi.)

Hãy xem xét chức năng này:

fubar :: Int -> (x -> y) -> y

Chức năng này là không thể . Bạn thực sự không thể thực hiện chức năng này. Tôi có thể nói rằng chỉ từ chữ ký loại.

Như bạn có thể thấy, một chữ ký loại Haskell cho bạn biết rất nhiều điều!


So sánh với C #. (Xin lỗi, Java của tôi hơi rỉ sét.)

public static TY foobar<TX, TY>(TX in1, TY in2)

Có một vài điều mà phương pháp này có thể làm:

  • Trả lại in2như kết quả.
  • Vòng lặp mãi mãi, và không bao giờ trả lại bất cứ điều gì.
  • Ném một ngoại lệ, và không bao giờ trả lại bất cứ điều gì.

Trên thực tế, Haskell cũng có ba tùy chọn này. Nhưng C # cũng cung cấp cho bạn các tùy chọn bổ sung:

  • Trả về null. (Haskell không có null.)
  • Sửa đổi in2trước khi trả lại nó. (Haskell không có sửa đổi tại chỗ.)
  • Sử dụng phản xạ. (Haskell không có phản xạ.)
  • Thực hiện nhiều hành động I / O trước khi trả về kết quả. (Haskell sẽ không cho phép bạn thực hiện I / O trừ khi bạn tuyên bố rằng bạn thực hiện I / O tại đây.)

Phản xạ là một cái búa đặc biệt lớn; bằng cách sử dụng sự phản chiếu, tôi có thể tạo ra một TYvật thể mới trong không khí mỏng và trả lại nó! Tôi có thể kiểm tra cả hai đối tượng và thực hiện các hành động khác nhau tùy thuộc vào những gì tôi tìm thấy. Tôi có thể thực hiện các sửa đổi tùy ý cho cả hai đối tượng được truyền vào.

I / O là một cái búa lớn tương tự. Mã này có thể hiển thị thông báo cho người dùng hoặc mở các kết nối cơ sở dữ liệu hoặc định dạng lại ổ cứng của bạn hoặc bất cứ thứ gì, thực sự.


foobarNgược lại, hàm Haskell chỉ có thể lấy một số dữ liệu và trả về dữ liệu đó, không thay đổi. Nó không thể "xem xét" dữ liệu, vì kiểu của nó không xác định tại thời gian biên dịch. Nó không thể tạo dữ liệu mới, bởi vì ... tốt, làm thế nào để bạn xây dựng dữ liệu thuộc bất kỳ loại nào có thể? Bạn sẽ cần sự phản ánh cho điều đó. Nó không thể thực hiện bất kỳ I / O nào, vì chữ ký loại không tuyên bố rằng I / O đang được thực hiện. Vì vậy, nó không thể tương tác với hệ thống tập tin hoặc mạng, hoặc thậm chí chạy các luồng trong cùng một chương trình! (Tức là, nó được đảm bảo 100% an toàn cho chuỗi.)

Như bạn có thể thấy, bằng cách không cho phép bạn làm cả đống thứ, Haskell đang cho phép bạn đảm bảo rất chắc chắn về những gì mã của bạn thực sự làm. Trên thực tế, rất chặt chẽ (đối với mã thực sự đa hình) thường chỉ có một cách khả thi là các mảnh có thể khớp với nhau.

(Để rõ ràng: Vẫn có thể viết các hàm Haskell trong đó chữ ký loại không cho bạn biết nhiều. Int -> IntCó thể là bất cứ thứ gì. Nhưng ngay cả khi đó, chúng ta biết rằng cùng một đầu vào sẽ luôn tạo ra cùng một đầu ra với độ tin cậy 100%. Java thậm chí không đảm bảo điều đó!)


4
+1 Câu trả lời tuyệt vời! Điều này rất mạnh mẽ và thường bị đánh giá thấp bởi những người mới đến Haskell. Nhân tiện, một chức năng "không thể" đơn giản hơn sẽ là fubar :: a -> b, phải không? (Vâng, tôi biết unsafeCoerce. Tôi cho rằng chúng ta không nói về bất cứ điều gì có "không an toàn" trong tên của nó, và những người mới không nên lo lắng về điều đó !: D)
Andres F.

Có rất nhiều chữ ký loại đơn giản hơn mà bạn không thể viết, vâng. Ví dụ, foobar :: xkhá khó thực hiện ...
Toán

Trên thực tế, bạn không thể làm cho chuỗi mã thuần không an toàn, nhưng bạn vẫn có thể làm cho nó đa luồng. Các tùy chọn của bạn là "trước khi bạn đánh giá điều này, đánh giá điều này", "khi bạn đánh giá điều này, bạn cũng có thể muốn đánh giá điều này trong một luồng riêng biệt" và "khi bạn đánh giá điều này, bạn cũng có thể muốn đánh giá điều này trong một chủ đề riêng biệt ". Mặc định là "làm như bạn muốn", về cơ bản có nghĩa là "đánh giá càng muộn càng tốt".
John Dvorak

Thông thường hơn, bạn có thể gọi các phương thức cá thể trên in1 hoặc in2 có tác dụng phụ. Hoặc bạn có thể sửa đổi trạng thái toàn cầu (được cấp, được mô hình hóa như một hành động IO trong Haskell, nhưng có thể không phải là điều mà hầu hết mọi người nghĩ về IO).
Doug McClean

2
@isomorphismes Loại x -> y -> yhoàn toàn có thể thực hiện được. Loại (x -> y) -> ykhông có. Loại x -> y -> ynày có hai đầu vào và trả về cái thứ hai. Kiểu (x -> y) -> ynày có một chức năng hoạt động xvà bằng cách nào đó phải tạo yra điều đó ...
Toán học

17

Một câu hỏi SO liên quan .

Tôi giả sử bạn có thể làm tương tự trong haskell (tức là nhét mọi thứ vào chuỗi / int thực sự phải là kiểu dữ liệu hạng nhất)

Không, bạn thực sự không thể - ít nhất là không giống như cách mà Java có thể. Trong Java, loại điều này xảy ra:

String x = (String)someNonString;

và Java sẽ vui vẻ thử và sử dụng Chuỗi không phải của bạn dưới dạng Chuỗi. Haskell không cho phép loại điều này, loại bỏ cả một lớp lỗi thời gian chạy.

nulllà một phần của hệ thống loại ( Nothingvì vậy) cần được yêu cầu và xử lý rõ ràng, loại bỏ toàn bộ các loại lỗi thời gian chạy khác.

Ngoài ra còn có một loạt các lợi ích tinh tế khác - đặc biệt là xung quanh việc tái sử dụng và các loại lớp học - mà tôi không có chuyên môn để biết đủ để giao tiếp.

Hầu hết, đó là bởi vì hệ thống loại của Haskell cho phép rất nhiều biểu cảm. Bạn có thể làm rất nhiều thứ chỉ với một vài quy tắc. Hãy xem xét cây Haskell luôn hiện diện:

data Tree a = Leaf a | Branch (Tree a) (Tree a) 

Bạn đã xác định toàn bộ cây nhị phân chung (và hai hàm tạo dữ liệu) trong một dòng mã khá dễ đọc. Tất cả chỉ sử dụng một vài quy tắc (có loại tổng và loại sản phẩm ). Đó là 3-4 tệp mã và các lớp trong Java.

Đặc biệt trong số những người có xu hướng hoàn nguyên các hệ thống, sự đồng nhất / thanh lịch này được đánh giá cao.


Tôi chỉ hiểu NullPulumExceptions từ câu trả lời của bạn. Bạn có thể bao gồm nhiều ví dụ?
Jesvin Jose

2
Không nhất thiết đúng, JLS §5.5.1 : Nếu T là loại lớp, thì | S | <: | T |, hoặc | T | <: | S |. Nếu không, một lỗi thời gian biên dịch xảy ra. Vì vậy, trình biên dịch sẽ không cho phép bạn truyền các loại không thể chuyển đổi - rõ ràng có nhiều cách xung quanh nó.
nhện của

Theo tôi, cách đơn giản nhất để đưa ra các lợi thế của các lớp loại là chúng giống như interfacecó thể được thêm vào sau thực tế và chúng không "quên" kiểu triển khai chúng. Nghĩa là, bạn có thể đảm bảo rằng hai đối số cho một hàm có cùng loại, không giống như interfaces trong đó hai List<String>s có thể có các triển khai khác nhau. Về mặt kỹ thuật, bạn có thể làm một cái gì đó rất giống với Java bằng cách thêm một tham số loại cho mọi giao diện, nhưng 99% giao diện hiện tại không làm điều đó và bạn sẽ nhầm lẫn giữa các đồng nghiệp.
Doval

2
@BoristheSpider Đúng, nhưng việc đưa ra các ngoại lệ hầu như luôn liên quan đến việc hạ âm từ một siêu lớp sang một lớp con hoặc từ một giao diện đến một lớp, và nó không phải là bất thường đối với siêu lớp Object.
Doval

2
Tôi nghĩ rằng vấn đề trong câu hỏi về chuỗi không liên quan đến lỗi kiểu truyền và thời gian chạy, nhưng thực tế là nếu bạn không muốn sử dụng các kiểu, Java sẽ không khiến bạn - như trong, thực sự lưu trữ dữ liệu của bạn theo tuần tự hình thức, lạm dụng các chuỗi như một anyloại ad-hoc . Haskell sẽ không ngăn bạn làm điều này, vì ... tốt, nó có chuỗi. Haskell có thể cung cấp cho bạn các công cụ, nó không thể ngăn bạn làm những điều ngu ngốc nếu bạn khăng khăng đòi Greenspucky đủ để một thông dịch viên phát minh lại nulltrong một bối cảnh lồng nhau. Không có ngôn ngữ có thể.
Leushenko

0

Một trong những 'meme' mà mọi người quen thuộc với nó thường nói đến, đó là toàn bộ "nếu nó biên dịch, nó sẽ hoạt động *" - điều mà tôi nghĩ có liên quan đến sức mạnh của hệ thống loại.

Điều này chủ yếu đúng với các chương trình nhỏ. Haskell ngăn bạn mắc lỗi dễ dàng trong các ngôn ngữ khác (ví dụ: so sánh một Int32và một Word32và một cái gì đó bùng nổ), nhưng nó không ngăn bạn khỏi tất cả các lỗi.

Haskell thực sự làm cho việc tái cấu trúc dễ dàng hơn rất nhiều . Nếu chương trình của bạn trước đây là chính xác và nó đánh máy, có một cơ hội công bằng, nó vẫn sẽ đúng sau khi sửa đổi nhỏ.

Tôi đang cố gắng để hiểu tại sao chính xác Haskell tốt hơn các ngôn ngữ gõ tĩnh khác trong vấn đề này.

Các loại trong Haskell khá nhẹ, trong đó dễ dàng khai báo các loại mới. Điều này trái ngược với một ngôn ngữ như Rust, nơi mọi thứ chỉ cồng kềnh hơn một chút.

Giả định của tôi là đây là ý nghĩa của mọi người đối với một hệ thống loại mạnh, nhưng đối với tôi thì không rõ tại sao Haskell lại tốt hơn.

Haskell có nhiều tính năng ngoài các loại sản phẩm và tổng đơn giản; nó cũng có các loại định lượng phổ biến (ví dụ id :: a -> a). Bạn cũng có thể tạo các loại bản ghi chứa các hàm, khác với ngôn ngữ như Java hoặc Rust.

GHC cũng có thể lấy được một số trường hợp chỉ dựa trên các loại và vì sự ra đời của thuốc generic, bạn có thể viết các hàm chung giữa các loại. Điều này khá thuận tiện và nó trôi chảy hơn so với Java.

Một sự khác biệt nữa là Haskell có xu hướng có lỗi loại tương đối tốt (ít nhất là bằng văn bản). Suy luận kiểu của Haskell rất phức tạp và khá hiếm khi bạn cần cung cấp chú thích loại để có được thứ gì đó để biên dịch. Điều này trái ngược với Rust, nơi suy luận kiểu đôi khi có thể yêu cầu chú thích ngay cả khi về nguyên tắc trình biên dịch có thể suy ra kiểu.

Cuối cùng, Haskell có máy đánh chữ, trong số đó là đơn nguyên nổi tiếng. Monads là một cách đặc biệt tốt để xử lý lỗi; về cơ bản chúng mang lại cho bạn gần như tất cả sự tiện lợi nullmà không cần gỡ lỗi khủng khiếp và không từ bỏ bất kỳ sự an toàn nào của bạn. Vì vậy, khả năng viết các hàm trên các loại này thực sự có vấn đề khá nhiều khi khuyến khích chúng ta sử dụng chúng!

Nói cách khác, bạn có thể viết Java tốt hay xấu, tôi giả sử bạn có thể làm tương tự trong Haskell

Điều đó có lẽ đúng, nhưng nó thiếu một điểm rất quan trọng: điểm mà bạn bắt đầu tự bắn vào chân mình ở Haskell là xa hơn so với điểm khi bạn bắt đầu tự bắn vào chân mình trong Java.

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.