2010-04-21 67 views
9

Tôi là một lập trình viên C++/Java và mô hình chính mà tôi tình cờ sử dụng trong lập trình hàng ngày là OOP. Trong một số chủ đề tôi đọc một bình luận rằng các lớp Type trực quan hơn trong tự nhiên hơn là OOP. Ai đó có thể giải thích khái niệm về các loại lớp trong những từ đơn giản để một người OOP như tôi có thể hiểu nó?Giải thích Loại Lớp học trong Haskell

+6

@Neil Butterworth: Tôi đã thêm các thẻ C++ và Java để những người biết một trong những ngôn ngữ này và cũng hiểu các loại lớp của haskell có thể giải thích chúng theo các thuật ngữ tôi có thể hiểu được. Không cần phải chỉnh sửa chúng. –

+1

Không có cách nào các lớp kiểu Haskell "trực quan hơn" so với OOP. Hệ thống kiểu lớp của Haskell bao gồm việc triển khai Prolog! (Datalog, thực sự). Và hầu hết các lập trình viên đều thấy lập trình logic khá không trực quan và khó học lúc đầu --- đó là tất cả về chứng minh. –

Trả lời

25

Đầu tiên, tôi luôn rất nghi ngờ về tuyên bố rằng đây hoặc cấu trúc chương trình là trực quan hơn. Lập trình phản trực giác và luôn luôn là vì mọi người tự nhiên nghĩ về các trường hợp cụ thể hơn là các quy tắc chung. Thay đổi điều này đòi hỏi phải đào tạo và thực hành, nếu không được gọi là "học lập trình".

Chuyển sang phần câu hỏi, sự khác biệt chính giữa các lớp OO và Haskell typeclasses là trong OO một lớp (thậm chí là một lớp giao diện) vừa là kiểu và mẫu cho các kiểu mới (hậu duệ). Trong Haskell, một kiểu chữ là chỉ một mẫu cho các loại mới. Chính xác hơn, một typeclass mô tả một tập hợp các loại có chung một giao diện, nhưng nó là không phải là một loại.

Vì vậy, typeclass "Num" mô tả các loại số với toán tử cộng, trừ và phép nhân. Kiểu "Integer" là một thể hiện của "Num", có nghĩa là Integer là một thành viên của tập hợp các kiểu thực hiện các toán tử đó.

Vì vậy, tôi có thể viết một hàm tổng với loại hình này:

sum :: Num a => [a] -> a 

Các chút để bên trái của "=>" nhà điều hành nói rằng "tổng" sẽ làm việc cho bất kỳ loại "a" có nghĩa là một thể hiện của Num. Các bit bên phải nói rằng nó có một danh sách các giá trị của loại "a" và trả về một giá trị duy nhất của loại "a" như là một kết quả. Vì vậy, bạn có thể sử dụng nó để tổng hợp một danh sách các số nguyên hoặc một danh sách các đôi hoặc một danh sách các phức tạp, bởi vì họ là tất cả các trường hợp của "Num". Việc thực hiện "tổng hợp" sẽ sử dụng toán tử "+" của khóa học, đó là lý do tại sao bạn cần ràng buộc loại "Num".

Tuy nhiên bạn không thể viết này:

sum :: [Num] -> Num 

vì "Num" không phải là một loại.

Sự khác biệt giữa loại và kiểu chữ là lý do tại sao chúng ta không nói về thừa kế và hậu duệ của các loại trong Haskell. Có một loại thừa kế cho typeclasses: bạn có thể khai báo một typeclass làm hậu duệ của một kiểu chữ khác. Con cháu ở đây mô tả một tập con của các kiểu được mô tả bởi cha mẹ.

Một hệ quả quan trọng của tất cả điều này là bạn không thể có danh sách không đồng nhất trong Haskell. Trong ví dụ "tổng hợp", bạn có thể chuyển nó một danh sách các số nguyên hoặc một danh sách các cặp đôi, nhưng bạn không thể trộn đôi và số nguyên trong cùng một danh sách.Điều này trông giống như một hạn chế khó khăn; làm thế nào bạn sẽ thực hiện "xe hơi và xe tải cũ" cả hai loại xe "ví dụ? Có một số câu trả lời tùy thuộc vào vấn đề bạn đang thực sự cố gắng giải quyết, nhưng nguyên tắc chung là bạn làm theo cách của bạn một cách rõ ràng bằng cách sử dụng các hàm hạng nhất thay vì ngầm sử dụng các hàm ảo.

+0

Superclasses từ ngữ cảnh là một lý do khá xin lỗi cho "thừa kế", mặc dù, vì họ yêu cầu bạn phải viết các trường hợp cho tất cả các lớp cha. Không phải đề cập đến các vấn đề khó chịu như "tại sao Functor không phải là siêu lớp của Monad" ... –

+12

Có được từ một thế giới OO trước khi học Haskell, tôi đã nghĩ đến sự thừa kế như một lý do xin lỗi cho các chức năng hạng nhất. –

+2

Functor là một siêu lớp của Monad. Bạn chỉ đang sử dụng triển khai sai Monad. – jrockway

3

Lớp loại có thể được so sánh với khái niệm 'triển khai' giao diện. Nếu một số kiểu dữ liệu trong Haskell thực hiện giao diện "Hiển thị", nó có thể được sử dụng với tất cả các hàm mong đợi một đối tượng "Hiển thị".

+3

... hơi giống như 'khái niệm' bị mất trong C++ ... – xtofl

12

Vâng, phiên bản ngắn là: Loại lớp học là những gì Haskell sử dụng cho tính đa hình ad-hoc.

... nhưng điều đó có thể không làm rõ bất kỳ điều gì cho bạn.

Đa hình phải là khái niệm quen thuộc đối với mọi người từ nền OOP. Điểm mấu chốt ở đây, tuy nhiên, là sự khác biệt giữa tham sốad-hoc đa hình.

Đa hình tham số có nghĩa là các hàm hoạt động trên kiểu cấu trúc được tham số hóa bởi các loại khác, chẳng hạn như danh sách giá trị. Đa hình tham số là khá nhiều tiêu chuẩn ở khắp mọi nơi trong Haskell; C# và Java gọi nó là "generics". Về cơ bản, một hàm tổng quát thực hiện điều tương tự đối với một cấu trúc cụ thể, bất kể tham số kiểu là gì.

Ad-hoc polymorphism, mặt khác, có nghĩa là một tập hợp các chức năng riêng biệt, làm (nhưng khái niệm có liên quan) những thứ khác nhau tuỳ theo từng loại. Không giống như đa hình tham số, các hàm đa hình ad-hoc cần phải được chỉ định riêng cho từng loại có thể được sử dụng với chúng. Đa hình Ad-hoc là một thuật ngữ tổng quát cho một loạt các tính năng được tìm thấy trong các ngôn ngữ khác, chẳng hạn như quá tải hàm trong C/C++ hoặc đa hình công văn dựa trên lớp trong OOP.

Điểm bán hàng chính của các loại lớp của Haskell so với các dạng đa hình ad-hoc khác linh hoạt hơn do cho phép đa hình bất kỳ nơi nào trong chữ ký loại. Ví dụ, hầu hết các ngôn ngữ sẽ không phân biệt các hàm bị quá tải dựa trên kiểu trả về; loại lớp học có thể.

Giao diện như được tìm thấy trong nhiều ngôn ngữ OOP tương tự như kiểu lớp của Haskell - bạn chỉ định nhóm tên/chữ ký mà bạn muốn xử lý theo kiểu đa hình, sau đó mô tả rõ ràng các loại khác nhau được sử dụng với các chức năng đó. Các lớp kiểu của Haskell được sử dụng tương tự, nhưng với độ linh hoạt cao hơn: bạn có thể viết chữ ký kiểu tùy ý cho các hàm kiểu lớp, với biến kiểu được sử dụng để chọn ví dụ xuất hiện ở bất cứ đâu bạn thích, không chỉ là kiểu của một đối tượng các phương thức đang được gọi.

Một số trình biên dịch Haskell - bao gồm phổ biến nhất, GHC - phần mở rộng phục vụ ngôn ngữ mà làm cho các lớp học kiểu thậm chí mạnh mẽ hơn, chẳng hạn như đa số các lớp học kiểu, cho phép bạn làm ad-hoc chức năng đa hình dựa trên công văn nhiều loại (tương tự như những gì được gọi là "nhiều công văn" trong OOP).


Để cố gắng cung cấp cho bạn một chút hương vị của nó, đây là một số mơ hồ Java/C# -flavored giả:

interface IApplicative<> 
{ 
    IApplicative<T> Pure<T>(T item); 
    IApplicative<U> Map<T, U>(Function<T, U> mapFunc, IApplicative<T> source); 
    IApplicative<U> Apply<T, U>(IApplicative<Function<T, U>> apFunc, IApplicative<T> source); 
} 

interface IReducible<> 
{ 
    U Reduce<T,U>(Function<T, U, U> reduceFunc, U seed, IReducible<T> source); 
} 

Lưu ý rằng chúng tôi, trong số những thứ khác, việc xác định một giao diện trên loại chung và xác định phương thức mà loại giao diện chỉ xuất hiện dưới dạng loại trả về , Pure. Không rõ ràng là mọi việc sử dụng tên giao diện nên có nghĩa là cùng một loại (nghĩa là, không pha trộn các loại khác nhau triển khai giao diện), nhưng tôi không chắc chắn cách thể hiện điều đó.

7

Ngoài những gì xtofl và camccann đã viết trong câu trả lời tuyệt vời của họ, một điều hữu ích để nhận thấy khi so sánh giao diện của Java đến các lớp học kiểu Haskell là như sau: giao diện

  1. Java là đóng, có nghĩa là tập hợp các giao diện bất kỳ thực hiện lớp đã cho nào được quyết định một lần và cho tất cả khi nào và ở đâu nó được định nghĩa;

  2. các lớp học kiểu Haskell là mở, có nghĩa là bất kỳ loại (hoặc nhóm các loại cho các lớp học kiểu đa tham số) có thể được thực hiện thành viên của bất kỳ lớp loại bất cứ lúc nào, miễn là các định nghĩa thích hợp có thể được cung cấp cho các hàm được định nghĩa bởi lớp loại.

Tính mở này của lớp loại (và giao thức của Clojure, rất giống nhau) là thuộc tính rất hữu ích; nó là khá bình thường đối với một lập trình viên Haskell để đưa ra một sự trừu tượng mới và ngay lập tức áp dụng nó vào một loạt các vấn đề liên quan đến các kiểu có sẵn thông qua việc sử dụng thông minh các lớp kiểu.

+1

Để công bằng, việc nhập các lớp đang mở cũng có những nhược điểm. Nhưng bạn phải đẩy hệ thống khá xa (và có thể sử dụng một số phần mở rộng GHC) để thực sự chạy vào chúng. –

+1

Nhược điểm cho biết có thể được cảm nhận dễ dàng bất cứ khi nào bạn thực hiện một trường hợp trẻ mồ côi. Nói lời tạm biệt với mô đun. Tôi tránh những đứa trẻ mồ côi như bệnh dịch hạch. – luqui

10

Trong C++/etc, "phương thức ảo" được gửi đi theo loại đối số ẩn this/self. (Phương pháp được trỏ đến trong bảng chức năng mà đối tượng ngầm trỏ đến)

Loại lớp hoạt động khác nhau và có thể thực hiện mọi thứ mà "giao diện" có thể và hơn thế nữa. Hãy bắt đầu với một ví dụ đơn giản về một cái gì đó mà giao diện không thể thực hiện: Lớp loại Read của Haskell.

ghci> -- this is a Haskell comment, like using "//" in C++ 
ghci> -- and ghci is an interactive Haskell shell 
ghci> 3 + read "5" -- Haskell syntax is different, in C: 3 + read("5") 
8 
ghci> sum (read "[3, 5]") -- [3, 5] is a list containing 3 and 5 
8 
ghci> -- let us find out the type of "read" 
ghci> :t read 
read :: (Read a) => String -> a 

read 's loại là (Read a) => String -> a, có nghĩa là cho tất cả các loại mà thực hiện loại hạng Read, read có thể chuyển đổi một String để loại đó. Đây là công văn dựa trên kiểu trả về, không thể với "giao diện".

Điều này không thể thực hiện theo cách tiếp cận của C++ et al, trong đó bảng chức năng được truy xuất từ ​​đối tượng - ở đây, bạn thậm chí không có đối tượng liên quan cho đến khi sau read trả về nó để bạn có thể gọi nó như thế nào?

Sự khác biệt về triển khai khóa từ giao diện cho phép điều này xảy ra, đó là bảng chức năng không được trỏ vào bên trong đối tượng, nó được chuyển riêng biệt bởi trình biên dịch đến các hàm được gọi.

Ngoài ra, trong C++/etc khi định nghĩa một lớp, chúng cũng chịu trách nhiệm triển khai giao diện của chúng. Điều này có nghĩa là bạn không thể phát minh ra giao diện mới và thực hiện Int hoặc std::vector triển khai giao diện đó.

Trong Haskell bạn có thể, và nó không phải là "khỉ vá" như trong Ruby. Haskell có một lược đồ khoảng cách tên tốt có nghĩa là hai lớp kiểu có thể có cả một hàm có cùng tên và một kiểu có thể vẫn triển khai cả hai.

này cho phép Haskell có nhiều lớp học đơn giản như Eq (loại có hỗ trợ kiểm tra đẳng thức), Show (loại mà có thể được in vào một String), Read (loại có thể được phân tích cú pháp từ một String), Monoid (loại mà có một hoạt động nối và một phần tử trống) và nhiều hơn nữa, và cho phép ngay cả các kiểu nguyên thủy như Int để triển khai các kiểu lớp thích hợp.

Với sự phong phú của các loại lớp, mọi người có khuynh hướng lập trình cho các loại tổng quát hơn và sau đó có nhiều chức năng tái sử dụng hơn và vì chúng cũng ít tự do hơn khi các loại này nói chung chúng thậm chí có thể tạo ra ít lỗi hơn!

TLDR: type-lớp == tuyệt vời

+1

Vâng, tôi không nghĩ rằng 'tổng hợp của bạn (đọc" [3, 5] ")' là một ví dụ tuyệt vời vì mặc dù nó hoạt động, 'sum (đọc" [3.1, 5.2] ")' thất bại với một lỗi (thậm chí mặc dù tất nhiên 'tổng hợp [3.1, 5.2]' thành công). Tôi nghi ngờ ví dụ của bạn chỉ hoạt động do loại quy tắc mặc định, đó là tiện dụng nhưng thẳng thắn một chút của một hack. –

+0

'Số nguyên có thể là các monoids theo nhiều cách. Làm cách nào để cung cấp bất kỳ tập hợp con tùy ý nào của chúng thành các cá thể 'Monoid'? ML modules == tuyệt vời. – pyon

1

Với OOP, bạn kế thừa cả giao diện và triển khai. Các lớp kiểu Haskell cho phép chúng được phân tách. Hai loại hoàn toàn không liên quan có thể hiển thị cùng một giao diện.

Có lẽ quan trọng hơn, Haskell cho phép triển khai lớp học được thêm vào "sau khi thực tế". Đó là, tôi có thể phát minh ra một số loại lớp mới của riêng tôi, và sau đó đi và làm cho tất cả các loại tiêu chuẩn được xác định trước là trường hợp của lớp này. Trong một ngôn ngữ OO, bạn [thường] không thể dễ dàng thêm một phương thức mới vào một lớp hiện có, cho dù nó có hữu ích như thế nào đi chăng nữa.

+0

JavaScript là một trong những ngôn ngữ cho phép bạn dễ dàng thêm/nguyên mẫu bất kỳ phương pháp nào bạn muốn "lớp "hoặc một đối tượng instantiated :) – CoR

+0

Vâng, JavaScript là loại khác thường như thế. Nó vẫn không phải là đặc biệt dễ dàng để đi tìm đối tượng _all_ của một loại nhất định và thay đổi tất cả. ;-) – MathematicalOrchid

Các vấn đề liên quan