Как сказал Ганеш, вы действительно можете использовать 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. При этом это забавное упражнение.