В отличие от других небезопасных * операций, документация для 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 абсолютно безопасен и предсказуем. И может быть достаточно хорошее приближение, которое служит многим практическим целям без нарушения ссылочной прозрачности.