Когда unsafeInterleaveIO небезопасно?
В отличие от других небезопасных * операций, документация для unsafeInterleaveIO
не совсем ясна о возможных ошибках. Так точно, когда это небезопасно? Я хотел бы знать условие как для параллельного/параллельного, так и для однопоточного использования.
В частности, две функции в следующем коде семантически эквивалентны? Если нет, когда и как?
joinIO :: IO a -> (a -> IO b) -> IO b
joinIO a f = do !x <- a
!x' <- f x
return x'
joinIO':: IO a -> (a -> IO b) -> IO b
joinIO' a f = do !x <- unsafeInterleaveIO a
!x' <- unsafeInterleaveIO $ f x
return x'
Вот как я буду использовать это на практике:
data LIO a = LIO {runLIO :: IO a}
instance Functor LIO where
fmap f (LIO a) = LIO (fmap f a)
instance Monad LIO where
return x = LIO $ return x
a >>= f = LIO $ lazily a >>= lazily . f
where
lazily = unsafeInterleaveIO . runLIO
iterateLIO :: (a -> LIO a) -> a -> LIO [a]
iterateLIO f x = do
x' <- f x
xs <- iterateLIO f x' -- IO monad would diverge here
return $ x:xs
limitLIO :: (a -> LIO a) -> a -> (a -> a -> Bool) -> LIO a
limitLIO f a converged = do
xs <- iterateLIO f a
return . snd . head . filter (uncurry converged) $ zip xs (tail xs)
root2 = runLIO $ limitLIO newtonLIO 1 converged
where
newtonLIO x = do () <- LIO $ print x
LIO $ print "lazy io"
return $ x - f x / f' x
f x = x^2 -2
f' x = 2 * x
converged x x' = abs (x-x') < 1E-15
Хотя я бы предпочел избежать использования этого кода в серьезных приложениях из-за ужасающего материала unsafe*
, я мог бы хотя бы быть более ленивым, чем это было бы возможно с более строгой монадой IO при решении вопроса о том, что означает "конвергенция", что приводит к (что Я думаю) более идиоматический Haskell. И это порождает еще один вопрос: почему это не семантика по умолчанию для монады Haskell (или GHC?)? Я слышал некоторые проблемы с управлением ресурсами для ленивого ввода-вывода (который GHC предоставляет только небольшой фиксированный набор команд), но примеры, как правило, несколько напоминают разбитый make файл: ресурс X зависит от ресурса Y, но если вы терпите неудачу для определения зависимости вы получаете статус undefined для X. Является ли ленивый IO виновником этой проблемы? (С другой стороны, если в приведенном выше коде есть тонкая ошибка concurrency, такая как взаимоблокировки, я бы воспринял ее как более фундаментальную проблему.)
Обновление
Рединг Бен и Дитрих ответ и его комментарии ниже, я кратко просмотрел исходный код ghc, чтобы увидеть, как монада IO реализована в GHC. Здесь я опускаю несколько моих выводов.
-
GHC реализует Haskell как нечистый, непересекаемо-прозрачный язык. Время работы GHC работает, последовательно оценивая нечистые функции с побочными эффектами, как и любые другие функциональные языки. Вот почему имеет значение порядок оценки.
-
unsafeInterleaveIO
является небезопасным, поскольку он может вводить любые ошибки concurrency даже в программе с потоком в сигле, подвергая (как правило) скрытую примесь GHC Haskell. (iteratee
кажется приятным и элегантным решением для этого, и я обязательно узнаю, как его использовать.)
-
монашка IO должна быть строгой, потому что безопасная, ленивая монашка IO потребует точного (поднятого) представления RealWorld, что кажется невозможным.
-
Это не только функции IO monad и unsafe
, которые являются небезопасными. Весь Haskell (как реализованный GHC) потенциально небезопасен, а "чистые" функции в (GHC) Haskell являются чистыми только по соглашению и доброй воле людей. Типы никогда не могут быть доказательством чистоты.
Чтобы увидеть это, я продемонстрирую, как GHC Haskell не является ссылочным прозрачным независимо от монады IO, независимо от функций unsafe*
и т.д.
-- An evil example of a function whose result depends on a particular
-- evaluation order without reference to unsafe* functions or even
-- the IO monad.
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE UnboxedTuples #-}
{-# LANGUAGE BangPatterns #-}
import GHC.Prim
f :: Int -> Int
f x = let v = myVar 1
-- removing the strictness in the following changes the result
!x' = h v x
in g v x'
g :: MutVar# RealWorld Int -> Int -> Int
g v x = let !y = addMyVar v 1
in x * y
h :: MutVar# RealWorld Int -> Int -> Int
h v x = let !y = readMyVar v
in x + y
myVar :: Int -> MutVar# (RealWorld) Int
myVar x =
case newMutVar# x realWorld# of
(# _ , v #) -> v
readMyVar :: MutVar# (RealWorld) Int -> Int
readMyVar v =
case readMutVar# v realWorld# of
(# _ , x #) -> x
addMyVar :: MutVar# (RealWorld) Int -> Int -> Int
addMyVar v x =
case readMutVar# v realWorld# of
(# s , y #) ->
case writeMutVar# v (x+y) s of
s' -> x + y
main = print $ f 1
Просто для упрощения ссылок я собрал некоторые из соответствующих определений
для монады IO, реализованной GHC.
(Все пути ниже относятся к верхней директории исходного хранилища ghc.)
-- Firstly, according to "libraries/base/GHC/IO.hs",
{-
The IO Monad is just an instance of the ST monad, where the state is
the real world. We use the exception mechanism (in GHC.Exception) to
implement IO exceptions.
...
-}
-- And indeed in "libraries/ghc-prim/GHC/Types.hs", We have
newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #))
-- And in "libraries/base/GHC/Base.lhs", we have the Monad instance for IO:
data RealWorld
instance Functor IO where
fmap f x = x >>= (return . f)
instance Monad IO where
m >> k = m >>= \ _ -> k
return = returnIO
(>>=) = bindIO
fail s = failIO s
returnIO :: a -> IO a
returnIO x = IO $ \ s -> (# s, x #)
bindIO :: IO a -> (a -> IO b) -> IO b
bindIO (IO m) k = IO $ \ s -> case m s of (# new_s, a #) -> unIO (k a) new_s
unIO :: IO a -> (State# RealWorld -> (# State# RealWorld, a #))
unIO (IO a) = a
-- Many of the unsafe* functions are defined in "libraries/base/GHC/IO.hs":
unsafePerformIO :: IO a -> a
unsafePerformIO m = unsafeDupablePerformIO (noDuplicate >> m)
unsafeDupablePerformIO :: IO a -> a
unsafeDupablePerformIO (IO m) = lazy (case m realWorld# of (# _, r #) -> r)
unsafeInterleaveIO :: IO a -> IO a
unsafeInterleaveIO m = unsafeDupableInterleaveIO (noDuplicate >> m)
unsafeDupableInterleaveIO :: IO a -> IO a
unsafeDupableInterleaveIO (IO m)
= IO ( \ s -> let
r = case m s of (# _, res #) -> res
in
(# s, r #))
noDuplicate :: IO ()
noDuplicate = IO $ \s -> case noDuplicate# s of s' -> (# s', () #)
-- The auto-generated file "libraries/ghc-prim/dist-install/build/autogen/GHC/Prim.hs"
-- list types of all the primitive impure functions. For example,
data MutVar# s a
data State# s
newMutVar# :: a -> State# s -> (# State# s,MutVar# s a #)
-- The actual implementations are found in "rts/PrimOps.cmm".
Итак, например, игнорируя конструктор и предполагая ссылочную прозрачность,
мы имеем
unsafeDupableInterleaveIO m >>= f
==> (let u = unsafeDupableInterleaveIO)
u m >>= f
==> (definition of (>>=) and ignore the constructor)
\s -> case u m s of
(# s',a' #) -> f a' s'
==> (definition of u and let snd# x = case x of (# _,r #) -> r)
\s -> case (let r = snd# (m s)
in (# s,r #)
) of
(# s',a' #) -> f a' s'
==>
\s -> let r = snd# (m s)
in
case (# s, r #) of
(# s', a' #) -> f a' s'
==>
\s -> f (snd# (m s)) s
Это не то, что мы обычно получаем от привязки обычных ленивых монадов.
Предполагая, что переменная состояния s
несет какое-то реальное значение (чего она не имеет), она больше похожа на параллельный IO (или чередующийся IO, как правильно говорит), чем ленивый IO, как мы обычно подразумеваем под "ленивой государственной монадой", в котором, несмотря на лени, состояния должным образом нарезаются ассоциативной операцией.
Я попытался реализовать истинно ленивую IO-монаду, но вскоре понял, что для того, чтобы определить ленивую монадическую композицию для типа данных ввода-вывода, мы должны иметь возможность поднять/разблокировать RealWorld
. Однако это кажется невозможным, потому что нет конструктора для State# s
и RealWorld
. И даже если бы это было возможно, я тогда должен был бы представить точное, функциональное представление нашего RealWorld, что тоже невозможно.
Но я все еще не уверен, что стандартный Haskell 2010 нарушает ссылочную прозрачность или ленивый IO сам по себе плох. По крайней мере, кажется вполне возможным построить небольшую модель RealWorld, на которой ленивый IO абсолютно безопасен и предсказуем. И может быть достаточно хорошее приближение, которое служит многим практическим целям без нарушения ссылочной прозрачности.
Ответы
Ответ 1
В верхней части две функции, которые у вас есть, всегда идентичны.
v1 = do !a <- x
y
v2 = do !a <- unsafeInterleaveIO x
y
Помните, что unsafeInterleaveIO
отменяет операцию IO
до тех пор, пока ее результат не будет принудительным, но вы сразу же вынуждаете его с помощью строчного соответствия шаблону !a
, поэтому операция не откладывается вообще. Таким образом, v1
и v2
являются точно такими же.
Обычно
В общем, вам нужно доказать, что ваше использование unsafeInterleaveIO
безопасно. Если вы вызываете unsafeInterleaveIO x
, то вам нужно доказать, что x
можно вызвать в любое время и все равно выдавать тот же результат.
Современные настроения относительно Lazy IO
... заключается в том, что Lazy IO опасен и плохая идея в 99% случаев.
Основная проблема, которую он пытается решить, заключается в том, что IO нужно делать в монаде IO
, но вы хотите иметь возможность инкрементного ввода-вывода, и вы не хотите переписывать все свои чистые функции вызовите вызовы IO для получения дополнительных данных. Инкрементный ввод-вывод важен, поскольку он использует меньше памяти, что позволяет работать с наборами данных, которые не вписываются в память, не изменяя слишком много алгоритмов.
Ленивое решение IO - это сделать IO вне монады IO
. Это обычно не безопасно.
Сегодня люди решают проблему инкрементного ввода-вывода по-разному, используя библиотеки, такие как Conduit или Pipes. Conduit and Pipes гораздо более детерминированы и хорошо себя ведут, чем Lazy IO, решают одни и те же проблемы и не требуют небезопасных конструкций.
Помните, что unsafeInterleaveIO
действительно просто unsafePerformIO
с другим типом.
Пример
Вот пример программы, которая сломана из-за ленивого ввода-вывода:
rot13 :: Char -> Char
rot13 x
| (x >= 'a' && x <= 'm') || (x >= 'A' && x <= 'M') = toEnum (fromEnum x + 13)
| (x >= 'n' && x <= 'z') || (x >= 'N' && x <= 'Z') = toEnum (fromEnum x - 13)
| otherwise = x
rot13file :: FilePath -> IO ()
rot13file path = do
x <- readFile path
let y = map rot13 x
writeFile path y
main = rot13file "test.txt"
Эта программа не будет работать. Замена ленивого ввода-вывода на строгий IO заставит его работать.
Ссылки
Из Lazy IO нарушает чистоту Олега Киселева в списке рассылки Haskell:
Мы демонстрируем, как ленивый IO прерывает ссылочную прозрачность. Чистый функция типа Int->Int->Int
дает разные целые числа в зависимости по порядку оценки его аргументов. Наш код Haskell98 использует ничего, кроме стандартного ввода. Мы заключаем, что превозношение чистоты Haskell и реклама ленивого ввода-вывода несовместимы.
...
Lazy IO не следует считать хорошим стилем. Один из общих определения чистоты заключаются в том, что чистые выражения должны одинаковые результаты независимо от порядка оценки, или что равные могут быть заменен на равных. Если выражение типа Int оценивает 1, мы должны иметь возможность заменить каждое вхождение выражения на 1 без изменения результатов и других наблюдаемых.
Из Lazy vs correct IO Олега Киселева в списке рассылки Haskell:
В конце концов, что может быть больше против дух Хаскелла, чем "чистая" функция с наблюдаемой стороной последствия. С Lazy IO действительно нужно выбирать между правильностью и производительность. Появление такого кода особенно странно после доказательства взаимоблокировок с Lazy IO, представленных в этом списке меньше, чем месяц назад. Не говоря уже о непредсказуемом использовании ресурсов и использование финализаторов для закрытия файлов (забывая, что GHC не гарантируют, что финализаторы будут запущены вообще).
Киселев написал библиотеку Iteratee, которая стала первой реальной альтернативой ленивому IO.
Ответ 2
Леность означает, что когда (и) выполняется ли фактически фактически вычисление, зависит от того, когда (и независимо от того, решает ли реализация выполнения) ее значение. Как программист Haskell вы полностью отказываетесь от контроля над порядком оценки (за исключением зависимостей данных, присущих вашему коду, и когда вы начинаете играть со строгостью, чтобы заставить среду выполнения делать определенные варианты).
Это отлично подходит для чистых вычислений, потому что результат чистого вычисления будет точно таким же, как только вы это сделаете (за исключением того, что если вы выполняете вычисления, которые вам действительно не нужны, вы можете столкнуться с ошибками или не сработать, когда другой оценочный порядок может позволить программе успешно завершаться, но все недвоенные значения, вычисленные любым порядком оценки, будут одинаковыми).
Но когда вы пишете IO-зависимый код, порядок оценки имеет значение. Вся цель IO
заключается в предоставлении механизма для построения вычислений, чьи этапы зависят и зависят от мира вне программы, и важной частью этого является то, что эти шаги явно упорядочены. Использование unsafeInterleaveIO
отменяет это явное упорядочивание и отменяет управление тем, когда (и независимо) выполняется ли операция IO
в системе времени выполнения.
Это небезопасно в целом для операций ввода-вывода, потому что между их побочными эффектами могут быть зависимости, которые не могут быть выведены из зависимостей данных внутри программы. Например, одно действие IO
может создать файл с некоторыми данными в нем, а другое действие IO
может прочитать тот же файл. Если они оба выполняются "лениво", тогда они будут запускаться только тогда, когда требуется полученное значение Haskell. Возможно, создание файла возможно IO ()
, и вполне возможно, что ()
никогда не понадобится. Это может означать, что сначала выполняется операция чтения, либо сбой или чтение данных, которые уже были в файле, но не данные, которые должны были быть добавлены другой операцией. Там нет гарантии, что система исполнения выполнит их в правильном порядке. Чтобы правильно программировать систему, которая всегда делала это для IO
, вам нужно было бы точно предсказать порядок, в котором время выполнения Haskell будет выбирать различные действия IO
.
Относитесь к unsafeInterlaveIO
в качестве обещания компилятору (который он не может проверить, он просто будет доверять вам), что он не имеет значения, когда выполняется действие IO
, или он полностью исчез. Это действительно все функции unsafe*
; они предоставляют объекты, которые небезопасны в целом и для которых безопасность не может быть автоматически проверена, но которая может быть безопасной в конкретных случаях. Ответственность за то, чтобы ваше использование было действительно безопасным. Но если вы пообещаете компилятору, и ваше обещание ложно, тогда могут возникнуть неприятные ошибки. "Небезопасно" в названии - это напугать вас, размышляя о вашем конкретном случае и решив, действительно ли вы можете выполнить обещание компилятору.
Ответ 3
В основном все под "Update" в вопросе настолько запутано, что это даже не так, поэтому, пожалуйста, постарайтесь забыть его, когда вы пытаетесь понять мой ответ.
Посмотрите на эту функцию:
badLazyReadlines :: Handle -> IO [String]
badLazyReadlines h = do
l <- unsafeInterleaveIO $ hGetLine h
r <- unsafeInterleaveIO $ badLazyReadlines h
return (l:r)
В дополнение к тому, что я пытаюсь проиллюстрировать: вышеупомянутая функция также не обрабатывает доступ к концу файла. Но игнорировать это пока.
main = do
h <- openFile "example.txt" ReadMode
lns <- badLazyReadlines h
putStrLn $ lns ! 4
Это будет печатать первую строку "example.txt", потому что 5-й элемент в списке фактически является первой строкой, которая читается из файла.
Ответ 4
Ваши joinIO
и joinIO'
не являются семантически эквивалентными. Они будут, как правило, одинаковыми, но в них есть тонкость: шаблон взлома делает значение строгим, но все, что он делает. Шаблоны Bang реализуются с использованием seq
, и это не обеспечивает выполнение определенного порядка оценки, в частности следующие два семантически эквивалентны:
a `seq` b `seq` c
b `seq` a `seq` c
GHC может оценить либо b, либо первый, прежде чем вернуться c. В самом деле, он может сначала оценить c, затем a и b, а затем вернуть c. Или, если он может статически доказать, что a или b не являются дном, или что c является дном, ему вообще не нужно оценивать a или b. Некоторые оптимизации действительно используют этот факт, но на практике это происходит нечасто.
unsafeInterleaveIO
, напротив, чувствителен ко всем или любым из этих изменений - он не зависит от семантического свойства того, насколько строгой является какая-то функция, а является эксплуатационным свойством того, когда что-то оценивается. Таким образом, все перечисленные выше преобразования видны ему, поэтому разумно рассматривать unsafeInterleaveIO
как выполняющую его IO недетерминистически, более или менее, когда это кажется нужным.
Это, по сути, почему unsafeInterleaveIO
небезопасно - это единственный механизм в обычном использовании, который может обнаруживать преобразования, которые должны сохраняться в значении. Это единственный способ обнаружить оценку, которая по праву должна быть невозможной.
В стороне, вероятно, справедливо мысленно добавлять unsafe
к каждой функции из GHC.Prim
и, возможно, к нескольким другим модулям GHC.
. Они, конечно, не обычные Haskell.