2015-03-10 16 views
10

Khi xác định một typeclass, làm thế nào để bạn quyết định giữa bao gồm/loại trừ một chức năng trong định nghĩa của typeclass? Ví dụ, sự khác biệt giữa 2 trường hợp là gì:Typeclasses: chức năng với thực hiện mặc định vs chức năng riêng biệt

class Graph g where 
    ... 

    insertNode :: g -> Node -> g 
    insertNode graph node = ... 

vs

class Graph g where 
    ... 

insertNode :: (Graph g) => g -> Node -> g 
insertNode graph node = ... 

Trả lời

9

Tôi nghĩ có một vài yếu tố gây căng thẳng ở đây. Có ý tưởng chung là định nghĩa loại lớp phải là tối thiểu và chỉ chứa độc lập chức năng.Vì câu trả lời của bhelkir giải thích, nếu lớp của bạn hỗ trợ các hàm a, bc, nhưng c có thể được triển khai theo số ab, đó là một đối số để xác định c bên ngoài lớp học.

Nhưng ý tưởng chung này gặp phải một số vấn đề xung đột khác.

Đầu tiên, thường có nhiều bộ hoạt động tối thiểu có thể xác định tương tự cùng một lớp. Định nghĩa cổ điển của Monad trong Haskell là này (dọn dẹp một chút):

class Monad m where 
    return :: a -> m a 
    (>>=) :: m a -> (a -> m b) -> m b 

Nhưng nó cũng biết rằng có những định nghĩa khác, như thế này một:

class Applicative m => Monad m where 
    join :: m (m a) -> m a 

return>>= là đủ để triển khai join, nhưng fmap, purejoin cũng đủ để triển khai >>=.

Điều tương tự với Applicative. Đây là kinh điển định nghĩa Haskell:

class Functor f => Applicative f where 
    pure :: a -> f a 
    (<*>) :: f (a -> b) -> f a -> f b 

Nhưng bất cứ điều nào sau đây là tương đương:

class Functor f => Applicative f where 
    unit :: f() 
    (<*>) :: f (a -> b) -> f a -> f b 

class Functor f => Applicative f where 
    pure :: a -> f a 
    fpair :: f a -> f b -> f (a, b) 

class Functor f => Applicative f where 
    unit :: f() 
    fpair :: f a -> f b -> f (a, b) 

class Functor f => Applicative f where 
    unit :: f() 
    liftA2 :: (a -> b -> c) -> f a -> f b -> f c 

Với bất kỳ các định nghĩa lớp, bạn có thể viết bất kỳ phương pháp trong bất kỳ những người khác như một hàm bắt nguồn bên ngoài lớp. Tại sao người đầu tiên được chọn? Tôi không thể trả lời tác giả, nhưng tôi nghĩ nó đưa chúng ta đến điểm thứ ba: cân nhắc hiệu suất. Các hoạt động fpair trong nhiều người kết hợp các giá trị f af b bằng cách tạo ra các bộ, nhưng đối với hầu hết cách dùng lớp Applicative chúng tôi không thực sự muốn những bộ dữ liệu, chúng tôi chỉ muốn kết hợp các giá trị rút ra từ f af b; định nghĩa kinh điển cho phép chúng ta chọn chức năng nào để thực hiện kết hợp này.

Một xem xét hiệu suất là ngay cả khi một số phương pháp trong một lớp có thể định nghĩa về người khác, những định nghĩa chung chung có thể không tối ưu cho tất cả các trường của lớp. Nếu chúng tôi lấy Foldable làm ví dụ, foldMapfoldr là không thể xác định được, nhưng một số loại hỗ trợ một cách hiệu quả hơn loại kia. Vì vậy, thông thường chúng ta có các định nghĩa lớp không tối thiểu để cho phép các cá thể cung cấp việc triển khai tối ưu các phương thức.

+1

Sự phân biệt nhỏ: '<,> 'không phải là tên nhà điều hành hợp pháp. –

+0

@ ØrjanJohansen: D'oh! Tôi đã đổi nó thành 'fpair' (tên được tạo thành, một chút tương tự với' fmap'). –

+0

Ưu điểm của việc giữ các loại lớp tối thiểu khi bổ sung của bạn ở mức tối thiểu có triển khai mặc định dựa trên giao diện tối thiểu là gì? – timdiels

7

Bao gồm một hàm trong định nghĩa của một typeclass có nghĩa là nó có thể được ghi đè. Trong trường hợp này, bạn sẽ cần cho nó ở bên trong kiểu chữ Graph vì nó trả về một Graph g => g và mỗi cá thể cụ thể của Graph sẽ cần phải biết cách xây dựng giá trị đó. Ngoài ra, bạn có thể chỉ định một hàm trong typeclass với mục đích xây dựng các giá trị kiểu Graph g => g và sau đó insertNode có thể sử dụng hàm đó trong kết quả của nó.

Giữ chức năng bên ngoài typeclass có nghĩa là nó không thể sửa đổi được, nhưng cũng không làm thay đổi lớp. Hãy xem xét ví dụ như hàm mapM. Không cần thiết phải ở trong lớp Monad và bạn có thể không muốn mọi người viết các triển khai của riêng mình là mapM, nó sẽ làm điều tương tự trong mọi ngữ cảnh. Một ví dụ khác, hãy xem xét các chức năng

-- f(x) = 1 + 3x^2 - 5x^3 + 10x^4 
aPoly :: Num a => a -> a 
aPoly x = 1 + 3 * x * x - 5 * x * x * x + 10 * x * x * x * x 

Rõ ràng aPoly không nên là một phần của Num typeclass, nó chỉ là một chức năng ngẫu nhiên điều đó xảy ra để sử dụng Num phương pháp. Nó không có gì để làm với những gì nó có nghĩa là một Num.

Thực sự, thiết kế sẽ không hoạt động. Các hàm thường được xác định trong một typeclass nếu chúng không thể tách rời với ý nghĩa của nó để trở thành một thể hiện của typeclass đó. Đôi khi các chức năng được bao gồm trong một typeclass nhưng với một định nghĩa mặc định để một loại cụ thể có thể quá tải nó để làm cho nó hiệu quả hơn, nhưng đối với hầu hết các phần nó làm cho tinh thần để giữ cho các thành viên lớp ở mức tối thiểu. Một cách để xem xét nó là bằng cách đặt câu hỏi "Có thể thực hiện chức năng này chỉ với ràng buộc của lớp không?" Nếu câu trả lời là không, nó phải ở trong lớp. Nếu câu trả lời là có, phần lớn thời gian nó có nghĩa là chức năng nên được di chuyển ra ngoài lớp học. Chỉ khi có giá trị thu được từ việc có thể quá tải, nó sẽ được chuyển vào lớp. Nếu quá tải chức năng đó có thể phá vỡ mã khác mà hy vọng nó hoạt động theo một cách cụ thể, sau đó nó không nên quá tải.

Một trường hợp khác cần xem xét là khi bạn có các hàm trong typeclass có mặc định sane, nhưng các giá trị mặc định đó phụ thuộc lẫn nhau. Lấy lớp Num làm ví dụ, bạn có

class Num a where 
    (+) :: a -> a -> a 
    (*) :: a -> a -> a 
    (-) :: a -> a -> a 
    a - b = a + negate b 
    negate :: a -> a 
    negate a = 0 - a 
    abs :: a -> a 
    signum :: a -> a 
    fromInteger :: Integer -> a 

ý rằng (-)negate đều thực hiện trong điều kiện của nhau. Nếu bạn tạo kiểu số của riêng mình thì bạn sẽ cần triển khai một hoặc cả hai số (-)negate, vì nếu không bạn sẽ có vòng lặp vô hạn trên tay. Đây là những chức năng hữu ích để quá tải, mặc dù, do đó, cả hai đều ở bên trong typeclass.

+0

Quá tải một hàm sai cách sẽ luôn phá vỡ mã. Bạn đang cố nói điều gì vậy? – dfeuer

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