полиморфизм хаскелла и списки - PullRequest
17 голосов
/ 15 июня 2009

Предположим, у меня есть следующее:

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) = ...

Можно ли как-нибудь определить список фигур, пройти по списку и вызвать функцию рисования для каждой фигуры? Следующий код не скомпилируется, потому что элементы списка имеют разный тип:

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

Я знаю, что я думаю оО и пытаюсь применить его к Хаскеллу, и это может быть не лучшим подходом. Каков наилучший подход Haskell для программ, которые должны работать с коллекциями различных типов объектов?

Ответы [ 6 ]

22 голосов
/ 15 июня 2009

Если вам действительно нужно это сделать, используйте 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)]

(я переименовал ваш класс, так как в противном случае произошло бы столкновение имен с типом данных, и такое переименование кажется более естественным).

Преимущество этого решения перед другим ответом, включающим один тип данных с разными конструкторами, состоит в том, что оно открыто ; Вы можете определить новые экземпляры IsShape, где захотите. Преимущество другого ответа заключается в том, что он более «функциональный», а также в некоторых случаях закрытость может быть преимуществом, поскольку это означает, что клиенты точно знают, чего ожидать.

13 голосов
/ 15 июня 2009

Рассмотрите возможность использования одного типа вместо отдельных типов и класса типов.

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]
5 голосов
/ 15 июня 2009

Один из способов сделать это - 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 = ...
}
5 голосов
/ 15 июня 2009

Как сказал Ганеш, вы действительно можете использовать GADT для большей безопасности типов. Но если вы не хотите (или не нуждаетесь), вот мое мнение:

Как вы уже знаете, все элементы списка должны быть одного типа. Не очень полезно иметь список элементов разных типов, потому что тогда вы выбрасываете информацию о вашем типе.

В этом случае, однако, поскольку вы хотите отбросить информацию о типе (вас просто интересует нарисованная часть значения), вы бы предложили изменить тип ваших значений на что-то, что просто рисуется.

type Drawable = IO ()

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

Предположительно, ваш фактический Drawable будет чем-то более интересным, чем просто IO () (может быть что-то вроде: MaxWidth -> IO ()).

А также, из-за ленивых вычислений, фактическое значение не будет отображено, пока вы не вызовете в списке что-то вроде sequence_. Так что вам не нужно беспокоиться о побочных эффектах (но вы, вероятно, уже видели это по типу shapes).


Просто для полноты (и включите мой комментарий в этот ответ): Это более общая реализация, полезная, если Shape имеет больше функций:

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)]

Здесь функция shape преобразует значение Shape a в значение ShapeResult, просто вызывая все функции класса Shape. Из-за лени ни одно из значений фактически не вычисляется, пока они вам не понадобятся.

Если честно, я не думаю, что на самом деле я бы использовал такую ​​конструкцию. Я бы либо использовал Drawable -метод сверху, либо, если требуется более общее решение, используйте GADT. При этом это забавное упражнение.

1 голос
/ 07 февраля 2013

Как работать с разнородным списком фигур в Haskell - Абстрактный полиморфизм с классами типов: http://pastebin.com/hL9ME7qP через @ pastebin

КОД:

{-# 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 голосов
/ 16 декабря 2014

Вариант решения Ганеша, использующий вместо этого синтаксис экзистенциального количественного определения.

{-# 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)]
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...