Ответ 1
Фон
Я отвечу на несколько более широкий (и более интересный) вопрос. Это связано с тем, что по крайней мере с семантической точки зрения имеется более одного конструктора ввода-вывода. Существует более одного "вида" значения IO
. Мы можем думать, что для печати на экран, возможно, есть один вид IO
, один вид значения IO
для чтения из файла и т.д.
Мы можем вообразить, что для обоснования IO определяется как нечто вроде
data IO a = ReadFile a
| WriteFile a
| Network a
| StdOut a
| StdIn a
...
| GenericIO a
с одним видом значения для любого вида действия IO
. (Тем не менее, имейте в виду, что на самом деле это не так, как реализовано IO
. IO
лучше всего не играть с игрушкой, если только вы не являетесь компилятором-хакером.)
Теперь интересный вопрос - почему они сделали это так, что мы не можем создать их вручную? Почему они не экспортировали эти конструкторы, чтобы мы могли их использовать? Это приводит к гораздо более широкому вопросу.
Почему вы хотите не экспортировать конструкторы для типа данных?
И для этого есть две причины: первая, вероятно, самая очевидная.
1. Конструкторы также являются деконструкторами
Если у вас есть доступ к конструктору, у вас также есть доступ к де-конструктору, с которым можно выполнить сопоставление шаблонов. Подумайте о типе Maybe a
. Если я дам вам значение Maybe
, вы можете извлечь все, что "внутри", что Maybe
с помощью сопоставления с образцом! Это легко.
getJust :: Maybe a -> a
getJust m = case m of
Just x -> x
Nothing -> error "blowing up!"
Представьте, можете ли вы сделать это с помощью IO
. Это означает, что IO
перестанет быть безопасным. Вы можете просто сделать то же самое внутри чистой функции.
getIO :: IO a -> a
getIO io = case io of
ReadFile s -> s
_ -> error "not from a file, blowing up!"
Это ужасно. Если у вас есть доступ к конструкторам IO
, вы можете создать функцию, которая превращает значение IO
в чистое значение. Это отстой.
Итак, это одна из причин не экспортировать конструкторы типа данных. Если вы хотите сохранить некоторые данные "секретными", вы должны хранить свои конструкторы в секрете, иначе кто-то может просто извлечь любые данные, которые они хотят с помощью сопоставления с образцом.
2. Вы не хотите разрешать любое значение
Эта причина будет знакома объектно-ориентированным программистам. Когда вы впервые изучаете объектно-ориентированное программирование, вы узнаете, что у объектов есть специальный метод, который вызывается при создании нового объекта. В этом методе вы также можете инициализировать значения полей внутри объекта, и самое лучшее - вы можете выполнить проверку работоспособности этих значений. Вы можете убедиться, что значения "имеют смысл" и выдают исключение, если они этого не делают.
Хорошо, вы можете сделать что-то одно в Haskell. Скажите, что вы компания с несколькими принтерами, и вы хотите отслеживать, сколько им лет и на каком этаже в здании они расположены. Таким образом, вы пишете программу Haskell. Ваши принтеры можно сохранить следующим образом:
data Printer = Printer { name :: String
, age :: Int
, floor :: Int
}
Теперь ваше здание имеет только 4 этажа, и вы не хотите, чтобы случайно сказали, что у вас есть принтер на этаже 14. Это можно сделать, не экспортируя конструктор Printer
, а вместо этого имея функцию mkPrinter
который создает для вас принтер, если все параметры имеют смысл.
mkPrinter :: String -> Int -> Maybe Printer
mkPrinter name floor =
if floor >= 1 && floor <= 4
then Just (Printer name 0 floor)
else Nothing
Если вы экспортируете эту функцию mkPrinter
, вы знаете, что никто не может создать принтер на несуществующем этаже.