Учитывая код Haskell:
do -- it's IO !!!
...
!x <- someFunction
... usage of x
насколько здесь полезен !
? Это результат монадической (это IO
!) оценки, поэтому мои вопросы:
если x
является примитивным типом, таким как UTCTime
(например, getCurrentTime
), Int
, Bool
- имеет ли !
смысл?
если у x
есть «голова», как у String
, Map k v
, может быть, какая-нибудь запись? Тогда x
можно лениться? Насколько ленив? Означает ли это, что когда я использую x
позже, происходит настоящее IO
?
Имеет ли IO
монада какое-то более специфическое поведение в этом контексте? Вроде немного более строгий, чем другие (более «чистые») монады?
По умолчанию ввод-вывод является ленивым, тогда как Haskell узнает, когда его вызвать немедленно (передавая это выражение) или когда заменить его переходником? По типу x
? Что-то вроде: GHC имеет список примитивных типов и все, что находится за пределами списка, подставляется?
Любые объяснения приветствуются.
Для вещей, требующих более глубокой оценки, обычно существуют варианты someFunction
с «глубокой последовательностью».
@willeM_VanOnsem Я имею в виду, что монада - это IO
, а не что-то вроде списка, Maybe или чего-то подобного.
Внутри монады IO !x <- ...
не совпадает с x <- ...
в общем случае.
Например,
do x <- (return undefined :: IO Bool)
print True
не дает сбоя, поскольку undefined
никогда не используется принудительно. Использование !x <- ...
вместо этого приведет к принудительному завершению работы и сбою.
Аналогично, если действие IO
заканчивается return (f y)
, где f
требуется много времени для вычисления, то x <- myIOaction
не сразу вызывает f y
(который будет оценен позже, когда и если x
потребуется), в то время как !x <- myIOaction
заставляет f y
.
IO
не является чем-то особенным в этом аспекте: после x <- action
переменная x
может быть связана с полностью вычисленным значением или невычисленным преобразователем, как и в (почти) любой другой монаде.
К сожалению, однозначного ответа просто не существует. В общем, вы должны знать реализацию someFunction
— и подробности о том, как x
используется — чтобы решить, является ли принудительное использование x
хорошей идеей.
Некоторые комментарии:
«По умолчанию ввод-вывод ленив» либо крайне вводит в заблуждение, либо совершенно неверен, в зависимости от того, что вы под этим подразумеваете. В m >>= f
эффекты m
всегда* происходят до того, как f
вызывается с аргументом.
Типа, возвращаемого действием, недостаточно, чтобы решить, стоит ли принудительное выполнение или нет. Например, можно было бы написать:
badTriangle :: Int -> Int
badTriangle n = foldr (+) 0 [0..n]
ioTriangle :: Int -> IO Int
ioTriangle = pure . badTriangle
Хотя Int
является примитивным типом, do !x <- ioTriangle n; ...
ведет себя совсем иначе, чем do x <- ioTriangle n; ...
. Особенно актуальным и довольно распространенным примером здесь является разница между getLine <&> read
, который генерирует исключения при использовании его значения (возможно, намного позже в программе), и getLine >>= readIO
(он же readLn
), который генерирует исключения при вводе неправильного значения.
В другом направлении getLine
сам по себе возвращает непримитивный тип, но принудительная обработка его результата не имеет никакого значения.
Знания о том, возвращает ли ваше действие удар или нет, недостаточно, чтобы решить, является ли принуждение хорошей идеей. В некоторых случаях вы можете знать, что значение в конечном итоге будет использовано и что полученное значение использует меньше памяти, чем преобразователь, использованный для его создания; тогда принуждение - хорошая идея. Но любое из этих условий может не сработать. У вас может быть программа, которая никогда не использует полученное значение; тогда немедленное форсирование может потребовать большого количества вычислений, которые никогда не окажутся полезными. Или у вас может быть очень маленькая мысль, описывающая очень большую величину; тогда немедленное форсирование может потребовать больших ассигнований сейчас, которые можно было бы отложить до более подходящего времени.
Однако допустимо и другое направление рассуждений: если вы знаете, что ваше действие не возвращает сигнал, то вам не нужно форсировать его результат.
Вся история становится еще интереснее и сложнее, когда вы вводите нити. Вы можете передавать переходы между потоками и контролировать, какой поток вызывает это, и это иногда полезно. Таким образом, даже знания того, что значение в конечном итоге будет использовано и оно мало, уже недостаточно — вы все равно можете оставить его невычисленным и передать эту работу какому-нибудь другому потоку.
* Хорошо-хорошо, есть несколько исключений, например readFile
и getContents
, все из которых где-то в своей реализации используют unsafeInterleaveIO
. Но это не нормальная ситуация.
x
— это неIO a
, у него есть типa
.