Вы уверены, что действительно хотите поместить в список различные типы?
Вы можете использовать что-то вроде jetxee с экзистенциальной квантификацией, но подумайте, что это на самом деле делает: у вас есть список терминов неизвестного типа, и единственное, что вы можете с ними сделать, это применить putOut
, чтобы вернуть значение IO ()
. То есть , если «интерфейс» предоставляет только одну функцию с известным типом результата, нет разницы между списком существующих и списком результатов . Единственное возможное использование первого включает преобразование его во второе, так зачем добавлять дополнительный промежуточный шаг? Вместо этого используйте что-то вроде этого:
main :: IO ()
main = do
sequence_ lst
where lst :: [IO ()]
lst = [out1 1, out2 1 2]
out1 x = putStrLn $ unwords ["Out1", show x]
out2 x y = putStrLn $ unwords ["Out2", show x, show y]
Поначалу это может показаться нелогичным, потому что оно опирается на некоторые необычные особенности Haskell. Рассмотрим:
- Никаких дополнительных вычислений не выполняется - ленивая оценка означает, что
show
, unwords
, & c. не будет запущен, если не выполнено действие IO
.
- Никакие побочные эффекты не связаны с простым созданием
IO ()
значений - они могут храниться в списках, передаваться в чистом коде и т. Д. Их выполняет только функция sequence_
в main
.
Тот же аргумент применяется к спискам "экземпляров Show
" и еще много чего. Это не хорошо работает для экземпляров чего-то вроде Eq
, где вам нужно два значения типа, но список экзистенциалов не будет работать лучше, потому что вы не знаете, есть ли два значения одного типа. Все, что вы могли бы сделать в в этом случае, - это проверить, чтобы каждый элемент был равен самому себе, а затем вы могли бы (как описано выше) просто создать список Bool
s и покончить с этим.
В более общих случаях лучше иметь в виду, что классы типа Haskell не являются интерфейсами ООП . Классы типов являются мощным средством реализации специального полиморфизма, но они не так хорошо подходят для сокрытия деталей реализации. ООП-языки обычно объединяют специальный полиморфизм, повторное использование кода, инкапсуляцию данных, поведенческий подтип и т. Д., Связывая все в одной иерархии классов; в Хаскеле вы можете (и часто должны ) иметь дело с каждым по отдельности.
Объект на языке ООП - это, грубо говоря, набор (скрытых, инкапсулированных) данных, связанных с функциями для манипулирования этими данными, каждый из которых принимает инкапсулированные данные в качестве неявного аргумента (this
, self
, так далее.). Чтобы повторить это в Haskell, вам вообще не нужны классы типов:
- Запишите каждый «метод класса» как обычную функцию с явно заданным параметром
self
.
- Частично применить каждую функцию к значению «инкапсулированных» данных
- Объединение частично примененных функций в один тип записи
Тип записи заменяет интерфейс ; любая коллекция функций с правильными сигнатурами представляет собой реализацию интерфейса. В некотором смысле это на самом деле лучший объектно-ориентированный стиль , потому что личные данные полностью скрыты, и только внешнее поведение раскрывается.
Как и в более простом случае выше, это почти точно эквивалентно экзистенциальной версии; запись функций - это то, что вы получите, применив каждый метод класса type к каждому экзистенциальному.
Существуют некоторые классы типов, в которых использование записи функций не будет работать хорошо - например, Monad
- которые, как правило, также являются теми же классами типов , которые не могут быть выражены обычными интерфейсами ООП. , как показывают современные версии C #, широко использующие монадический стиль, но не обеспечивающие какого-либо общего интерфейса IMonad
.
См. Также эту статью , охватывающую то же самое, что я говорю. Вы также можете посмотреть на Graphics.DrawingCombinators для примера библиотеки, предлагающей расширяемую, компонуемую графику без использования классов типов .