2009-06-15 34 views
16

Giả sử tôi đã điều sau đây:Haskell đa hình và danh sách

class Shape a where 
    draw a :: a -> IO() 

data Rectangle = Rectangle Int Int 

instance Shape Rectangle where 
    draw (Rectangle length width) = ... 

data Circle = Circle Int Int 

instance Shape Circle where 
    draw (Circle center radius) = ... 

Có cách nào cho tôi để xác định một danh sách các hình dạng, traverse trên danh sách, và gọi hàm bốc thăm trên mỗi hình? Mã sau sẽ không biên dịch vì các phần tử danh sách không phải là cùng một loại:

shapes = [(Circle 5 10), (Circle 20, 30), (Rectangle 10 15)] 

Tôi biết tôi đang cố gắng áp dụng nó cho Haskell và có thể không phải là cách tiếp cận tốt nhất. Điều gì sẽ là cách tiếp cận Haskell tốt nhất cho các chương trình cần phải đối phó với các bộ sưu tập của các loại đối tượng khác nhau?

Trả lời

21

Nếu bạn thực sự cần phải làm điều này, sau đó sử dụng một existential:

{-# LANGUAGE GADTs #-} 


class IsShape a where 
    draw :: a -> IO() 

data Rectangle = Rectangle Int Int 

instance IsShape Rectangle where 
    draw (Rectangle length width) = ... 

data Circle = Circle Int Int 

instance IsShape Circle where 
    draw (Circle center radius) = ... 

data Shape where 
    Shape :: IsShape a => a -> Shape 

shapes = [Shape (Circle 5 10), Shape (Circle 20 30), Shape (Rectangle 10 15)] 

(tôi đổi tên lớp học của bạn như sẽ có một cuộc đụng độ tên với kiểu dữ liệu khác, và có việc đặt tên này cách vòng dường như tự nhiên hơn).

Lợi thế của giải pháp này so với câu trả lời khác liên quan đến một kiểu dữ liệu đơn với các nhà xây dựng khác nhau là nó là mở; bạn có thể xác định các phiên bản mới của IsShape ở bất cứ đâu bạn thích. Lợi thế của câu trả lời khác là nó có nhiều chức năng hơn, và cũng có thể là sự đóng cửa trong một số trường hợp là một lợi thế vì nó có nghĩa là khách hàng biết chính xác những gì mong đợi.

+1

Tôi không thể lấy ví dụ của bạn để biên dịch. Nhưng, trang wiki bạn tham khảo trả lời câu hỏi của tôi một cách hoàn hảo. –

+0

Thử ngay bây giờ - Tôi đã có một 'Hình dạng', nơi tôi đã có một' IsShape' trong chữ ký của hàm tạo dữ liệu cho 'Hình dạng'. –

+2

Cần chú ý đến độc giả trong tương lai rằng nhược điểm của phương pháp này là mã nhận các vòng tròn và hình chữ nhật ra khỏi thùng chứa 'Shape' sẽ không thể sử dụng * bất kỳ * thuộc tính nào khác so với những gì được đưa ra trong cá thể' IsShape'; bạn không thể nhận được tọa độ, không thể biết đó là 'Vòng tròn' hay 'Hình chữ nhật', và không thể gọi bất kỳ chức năng nào khác hoạt động đặc biệt trên hình tròn hoặc hình chữ nhật, thay vì bất kỳ hình dạng nào. Đó là một hệ quả cơ bản của sự cởi mở mà Ganesh đang nói đến (và xuất hiện trong lập trình OO được gõ tĩnh khi bạn cảm thấy bị buộc phải "downcast"). – Ben

13

Cân nhắc sử dụng một loại duy nhất thay vì các loại riêng biệt và một kiểu chữ.

data Shape = Rectangle Int Int 
      | Circle Int Int 

draw (Rectangle length width) = ... 
draw (Circle center radius) = ... 

shapes = [Circle 5 10, Circle 20 30, Rectangle 10 15] 
+6

Trong khi điều này có tác dụng –

5

Như Ganesh đã nói, bạn thực sự có thể sử dụng GADT để có độ an toàn cao hơn. Nhưng nếu bạn không muốn (hoặc cần), đây là việc tôi thực hiện việc này:

Như bạn đã biết, tất cả các thành phần của danh sách cần phải cùng loại. Nó không phải là rất hữu ích để có một danh sách các yếu tố của các loại khác nhau, bởi vì sau đó ném đi thông tin loại của bạn. Tuy nhiên, trong trường hợp này, vì bạn muốn loại bỏ thông tin loại (bạn chỉ quan tâm đến phần có thể rút được của giá trị), bạn nên thay đổi loại giá trị của mình thành một thứ có thể rút ra được.

type Drawable = IO() 

shapes :: [Drawable] 
shapes = [draw (Circle 5 10), draw (Circle 20 30), draw (Rectangle 10 15)] 

Có lẽ, thực tế Drawable sẽ được cái gì của bạn thú vị hơn chỉ IO() (có lẽ cái gì đó như: MaxWidth -> IO()).

Và cũng có thể, do đánh giá lười biếng, giá trị thực tế sẽ không được rút ra cho đến khi bạn buộc danh sách có thứ gì đó như sequence_. Vì vậy, bạn không phải lo lắng về tác dụng phụ (nhưng bạn có thể đã thấy rằng từ loại shapes).


Chỉ cần được đầy đủ (và kết hợp nhận xét của tôi vào câu trả lời này): Đây là một việc thực hiện tổng quát hơn, hữu ích nếu Shape có nhiều chức năng:

type MaxWith = Int 

class Shape a where 
    draw :: a -> MaxWidth -> IO() 
    size :: a -> Int 

type ShapeResult = (MaxWidth -> IO(), Int) 

shape :: (Shape a) => a -> ShapeResult 
shape x = (draw x, size x) 

shapes :: [ShapeResult] 
shapes = [shape (Circle 5 10), shape (Circle 20 30), shape (Rectangle 10 15)] 

Ở đây, shape chức năng biến đổi một Shape a giá trị thành một giá trị ShapeResult, đơn giản bằng cách gọi tất cả các hàm trong lớp Shape.Do lười biếng, không có giá trị nào thực sự được tính cho đến khi bạn cần chúng.

Thành thật mà nói, tôi không nghĩ rằng tôi thực sự sẽ sử dụng một cấu trúc như thế này. Tôi sẽ sử dụng phương pháp Drawable từ trên cao, hoặc nếu cần một giải pháp tổng quát hơn, hãy sử dụng GADT. Điều đó đang được nói, đây là một bài tập thú vị.

+0

Điều này là tốt đẹp nếu vẽ thực sự là tất cả những gì bạn muốn làm với chúng - nếu bạn có thể muốn làm những thứ khác nhau từ lớp 'Shape', thì điều này không quy mô tốt - mặc dù trong một số giác quan, giải pháp của tôi là tự nhiên mở rộng cách tiếp cận này khi nó đi qua toàn bộ từ điển lớp học với mỗi giá trị mà bạn có thể áp dụng như bạn muốn. –

+0

Chắc chắn, điều này chỉ hoạt động nếu 'Hình dạng' không có gì hơn' vẽ'. Khi tôi bắt đầu viết, tôi có một 'Drawable' _data type_, với một trường' IO() 'duy nhất, và một hàm' toDrawable :: (Shape a) => a -> Drawable'. Điều đó có thể mở rộng đến nhiều trường hơn (cho mỗi hàm trong 'Hình dạng') và tất nhiên đó chỉ là từ điển lớp được xây dựng thủ công ... –

+0

Nhận xét của Ganesh cũng là phản ứng của tôi đối với điều này. Nó cho thấy một cách sử dụng thực sự thú vị về đánh giá lười biếng của Haskell. Đó là loại mát mẻ. Là người mới với Haskell, tôi phải thừa nhận rằng nó sẽ đưa tôi một chút để làm quen với việc tận dụng đánh giá lười biếng. –

5

Một cách để làm điều đó sẽ là với vtables:

data Shape = Shape { 
    draw :: IO(), 
    etc :: ... 
} 

rectangle :: Int -> Int -> Shape 
rectangle len width = Shape { 
    draw = ..., 
    etc = ... 
} 

circle :: Int -> Int -> Shape 
circle center radius = Shape { 
    draw = ..., 
    etc = ... 
} 
+1

Vấn đề ở đây là kiểu dữ liệu Hình dạng của bạn phải là một liên minh của tất cả các trường của tất cả các hình dạng có thể. Điều đó có thể làm việc tốt cho trường hợp của tôi (contrived), nhưng nó sẽ là một cách tiếp cận khó xử trong các trường hợp phức tạp hơn. –

+1

Câu trả lời thú vị, đã không nghĩ về một cái gì đó như thế này. @Clint: Tôi không nghĩ rằng kiểu dữ liệu 'Hình dạng 'ở đây có thể là sự kết hợp của tất cả các trường có thể. Nó mô tả tất cả các hàm trong lớp 'Shape' của bạn, mỗi hàm khởi tạo (hình chữ nhật, hình tròn) tạo ra một hình dạng bằng cách cung cấp một triển khai duy nhất cho các hàm, giống như với các cá thể. –

+1

Điều này về cơ bản là làm việc với các từ điển loại lớp rõ ràng. Nó hoạt động, nhưng phần lớn các từ điển tiềm ẩn đơn giản hơn rất nhiều. Giải pháp dựa trên sự tồn tại mà tôi đã đăng sẽ kết thúc giống như trong phần trình bày (ít nhất là với một trình biên dịch như GHC sử dụng các từ điển để triển khai các lớp kiểu). –

1

Làm thế nào để đối phó với một danh sách không đồng nhất của các hình dạng trong Haskell - Tóm tắt đa hình với các lớp loại: http://pastebin.com/hL9ME7qP qua @pastebin

Mã sản phẩm:

{-# LANGUAGE GADTs #-} 

class Shape s where 
area :: s -> Double 
perimeter :: s -> Double 

data Rectangle = Rectangle { 
width :: Double, 
height :: Double 
} deriving Show 

instance Shape Rectangle where 
area rectangle = (width rectangle) * (height rectangle) 
perimeter rectangle = 2 * ((width rectangle) + (height rectangle)) 

data Circle = Circle { 
radius :: Double 
} deriving Show 

instance Shape Circle where 
area circle = pi * (radius circle) * (radius circle) 
perimeter circle = 2.0 * pi * (radius circle) 

r=Rectangle 10.0 3.0 
c=Circle 10.0 
list=[WrapShape r,WrapShape c] 

data ShapeWrapper where 
WrapShape :: Shape s => s -> ShapeWrapper 

getArea :: ShapeWrapper -> Double 
getArea (WrapShape s) = area s 

getPerimeter :: ShapeWrapper -> Double 
getPerimeter (WrapShape s) = perimeter s 

areas = map getArea list 
perimeters = map getPerimeter list 
0

Một biến thể của giải pháp Ganesh sử dụng cú pháp định lượng hiện tại thay thế.

{-# LANGUAGE ExistentialQuantification #-} 
class IsShape a where 
    draw :: a -> String 

data Rectangle = Rectangle Int Int 

instance IsShape Rectangle where 
    draw (Rectangle length width) = "" 

data Circle = Circle Int Int 

instance IsShape Circle where 
    draw (Circle center radius) = "" 

data Shape = forall a. (IsShape a) => Shape a 

shapes = [Shape (Circle 5 10), Shape (Circle 20 30), Shape (Rectangle 10 15)] 
Các vấn đề liên quan