Почему я не могу этого сделать:
import Data.Char
getBool = do
c <- getChar
if c == 't'
then IO True
else IO False
вместо return
?
Почему я не могу этого сделать:
import Data.Char
getBool = do
c <- getChar
if c == 't'
then IO True
else IO False
вместо return
?
Я отвечу на несколько более широкий (и более интересный) вопрос. Это связано с тем, что по крайней мере с семантической точки зрения имеется более одного конструктора ввода-вывода. Существует более одного "вида" значения IO
. Мы можем думать, что для печати на экран, возможно, есть один вид IO
, один вид значения IO
для чтения из файла и т.д.
Мы можем вообразить, что для обоснования IO определяется как нечто вроде
data IO a = ReadFile a
| WriteFile a
| Network a
| StdOut a
| StdIn a
...
| GenericIO a
с одним видом значения для любого вида действия IO
. (Тем не менее, имейте в виду, что на самом деле это не так, как реализовано IO
. IO
лучше всего не играть с игрушкой, если только вы не являетесь компилятором-хакером.)
Теперь интересный вопрос - почему они сделали это так, что мы не можем создать их вручную? Почему они не экспортировали эти конструкторы, чтобы мы могли их использовать? Это приводит к гораздо более широкому вопросу.
И для этого есть две причины: первая, вероятно, самая очевидная.
Если у вас есть доступ к конструктору, у вас также есть доступ к де-конструктору, с которым можно выполнить сопоставление шаблонов. Подумайте о типе 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
в чистое значение. Это отстой.
Итак, это одна из причин не экспортировать конструкторы типа данных. Если вы хотите сохранить некоторые данные "секретными", вы должны хранить свои конструкторы в секрете, иначе кто-то может просто извлечь любые данные, которые они хотят с помощью сопоставления с образцом.
Эта причина будет знакома объектно-ориентированным программистам. Когда вы впервые изучаете объектно-ориентированное программирование, вы узнаете, что у объектов есть специальный метод, который вызывается при создании нового объекта. В этом методе вы также можете инициализировать значения полей внутри объекта, и самое лучшее - вы можете выполнить проверку работоспособности этих значений. Вы можете убедиться, что значения "имеют смысл" и выдают исключение, если они этого не делают.
Хорошо, вы можете сделать что-то одно в 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
, вы знаете, что никто не может создать принтер на несуществующем этаже.
Вы можете использовать IO
вместо return
. Но это не так просто. И вам также нужно импортировать некоторые внутренние модули.
Посмотрим на источник Control.Monad
:
instance Monad IO where
{-# INLINE return #-}
{-# INLINE (>>) #-}
{-# INLINE (>>=) #-}
m >> k = m >>= \ _ -> k
return = returnIO
(>>=) = bindIO
fail s = failIO s
returnIO :: a -> IO a
returnIO x = IO $ \ s -> (# s, x #)
Но даже для использования IO
вместо return
вам нужно импортировать GHC.Types(IO(..))
:
newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #))
После этого вы можете написать IO $ \ s -> (# s, True #)
(IO
is a State) вместо return True
:
Решение:
{-# LANGUAGE UnboxedTuples #-} -- for unboxed tuples (# a, b #)
{-# LANGUAGE TupleSections #-} -- then (,b) == \a -> (a, b)
import GHC.Types (IO (..))
import Data.Char
getBool = do
c <- getChar
if c == 't'
then IO (# , True #)
else IO (# , False #)
В моделях IO
и ST
очень мало магии, намного меньше, чем полагает большинство людей.
Страшный тип ввода-вывода - это просто newtype
, определенный в GHC.Prim:
newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #))
Прежде всего, как видно выше, аргумент конструктора IO
не совпадает с аргументом return
. Вы можете получить лучшую идею, посмотрев наивную реализацию монады State
:
newtype State s a = State (s -> (s, a))
Во-вторых, IO является абстрактным типом: это намеренное решение не экспортировать конструктор, чтобы вы не могли построить конструкцию IO
или шаблон. Это позволяет Haskell обеспечивать ссылочную прозрачность и другие полезные свойства даже при наличии ввода-вывода.