Есть ли когда-нибудь веская причина для использования unsafePerformIO?
Вопрос говорит обо всем. Более конкретно, я пишу привязки к библиотеке C, и мне интересно, какие функции c я могу использовать unsafePerformIO
с. Я предполагаю, используя unsafePerformIO
с чем-либо, связанным с указателями, это большое нет-нет.
Было бы здорово увидеть другие случаи, когда это приемлемо для использования unsafePerformIO
тоже.
6 ответов
В конкретном случае FFI, unsafePerformIO
предназначен для вызова вещей, которые являются математическими функциями, т.е. вывод зависит исключительно от входных параметров, и каждый раз, когда функция вызывается с одними и теми же входами, она будет возвращать один и тот же результат. Кроме того, функция не должна иметь побочных эффектов, таких как изменение данных на диске или изменение памяти.
Большинство функций из <math.h>
может быть вызван с unsafePerformIO
, например.
Ты прав, что unsafePerformIO
и указатели обычно не смешиваются. Например, предположим, у вас есть
p_sin(double *p) { return sin(*p); }
Даже если вы просто читаете значение из указателя, его использование небезопасно unsafePerformIO
, Если вы заверните p_sin
Несколько вызовов могут использовать аргумент указателя, но получить разные результаты. Необходимо сохранить функцию в IO
чтобы убедиться в правильности его последовательности в отношении обновлений указателя.
Этот пример должен прояснить одну причину, почему это небезопасно:
# file export.c
#include <math.h>
double p_sin(double *p) { return sin(*p); }
# file main.hs
{-# LANGUAGE ForeignFunctionInterface #-}
import Foreign.Ptr
import Foreign.Marshal.Alloc
import Foreign.Storable
foreign import ccall "p_sin"
p_sin :: Ptr Double -> Double
foreign import ccall "p_sin"
safeSin :: Ptr Double -> IO Double
main :: IO ()
main = do
p <- malloc
let sin1 = p_sin p
sin2 = safeSin p
poke p 0
putStrLn $ "unsafe: " ++ show sin1
sin2 >>= \x -> putStrLn $ "safe: " ++ show x
poke p 1
putStrLn $ "unsafe: " ++ show sin1
sin2 >>= \x -> putStrLn $ "safe: " ++ show x
При компиляции эта программа выводит
$ ./main
unsafe: 0.0
safe: 0.0
unsafe: 0.0
safe: 0.8414709848078965
Даже если значение, на которое ссылается указатель, изменилось между двумя ссылками на "sin1", выражение не переоценивается, что приводит к использованию устаревших данных. поскольку safeSin
(и поэтому sin2
) находится в IO, программа вынуждена пересмотреть выражение, поэтому вместо этого используются обновленные данные указателя.
Нет необходимости привлекать C здесь. unsafePerformIO
Функция может быть использована в любой ситуации, когда,
Вы знаете, что его использование безопасно, и
Вы не можете доказать его безопасность, используя систему типов Haskell.
Например, вы можете сделать функцию памятки, используя unsafePerformIO
:
memoize :: Ord a => (a -> b) -> a -> b
memoize f = unsafePerformIO $ do
memo <- newMVar $ Map.empty
return $ \x -> unsafePerformIO $ modifyMVar memo $ \memov ->
return $ case Map.lookup x memov of
Just y -> (memov, y)
Nothing -> let y = f x
in (Map.insert x y memov, y)
(Это не в моей голове, поэтому я понятия не имею, есть ли в коде вопиющие ошибки.)
Функция memoize использует и изменяет словарь напоминаний, но поскольку функция в целом безопасна, вы можете задать ей чистый тип (без использования IO
монада). Тем не менее, вы должны использовать unsafePerformIO
сделать это.
Сноска. Когда дело доходит до FFI, вы несете ответственность за предоставление типов функций C системе Haskell. Вы можете добиться эффекта unsafePerformIO
просто опуская IO
от типа. Система FFI по своей сути небезопасна, поэтому использование unsafePerformIO
не имеет большого значения.
Сноска 2. В коде, который использует unsafePerformIO
Примером является лишь эскиз возможного использования. Особенно, unsafePerformIO
может плохо взаимодействовать с оптимизатором.
Очевидно, что если его никогда не использовать, его не будет в стандартных библиотеках.;-)
Есть ряд причин, по которым вы можете использовать его. Примеры включают в себя:
Инициализация глобального изменяемого состояния. (Должны ли вы когда-либо иметь такую вещь во-первых, это совсем другое обсуждение...)
Ленивый ввод / вывод реализован с использованием этого трюка. (Опять же, вопрос о том, является ли ленивый ввод-вывод хорошей идеей, спорен.)
trace
функция использует это. (Еще раз получаетсяtrace
скорее менее полезен, чем вы можете себе представить.)Возможно, наиболее важно то, что вы можете использовать его для реализации структур данных, которые являются ссылочно прозрачными, но внутренне реализованы с использованием нечистого кода. Часто
ST
Монада позволит вам сделать это, но иногда вам нужно немногоunsafePerformIO
,
Ленивый ввод / вывод можно рассматривать как частный случай последней точки. Так может запоминание.
Рассмотрим, например, "неизменный", растущий массив. Внутренне вы можете реализовать это как чистый "дескриптор", который указывает на изменяемый массив. Дескриптор содержит видимый пользователю размер массива, но фактический базовый изменяемый массив больше этого. Когда пользователь "добавляет" к массиву, возвращается новый дескриптор, с новым, большим размером, но добавление выполняется путем изменения базового изменяемого массива.
Вы не можете сделать это с ST
монада. (Вернее, можно, но все равно требуется unsafePerformIO
.)
Обратите внимание, что это чертовски сложно сделать все правильно. И средство проверки типов не поймает, если вы ошибаетесь. (Что к чему unsafePerformIO
делает; это заставляет средство проверки типов не проверять, правильно ли вы это делаете!) Например, если вы добавляете к "старому" дескриптору, правильное действие будет состоять в том, чтобы скопировать базовый изменяемый массив. Забудьте об этом, и ваш код будет вести себя очень странно.
Теперь, чтобы ответить на ваш реальный вопрос: нет особой причины, почему "что-либо без указателей" должно быть нет-нет для unsafePerformIO
, Отвечая на вопрос, использовать эту функцию или нет, единственный важный вопрос заключается в следующем: может ли конечный пользователь наблюдать какие-либо побочные эффекты от этого?
Если единственное, что он делает, это создает где-то буфер, который пользователь не может "увидеть" из чистого кода, это нормально. Если он пишет в файл на диске... не так хорошо.
НТН.
Стандартный трюк для создания глобальных изменяемых переменных в haskell:
{-# NOINLINE bla #-}
bla :: IORef Int
bla = unsafePerformIO (newIORef 10)
Я также использую его для закрытия глобальной переменной, если я хочу запретить доступ к ней вне функций, которые я предоставляю:
{-# NOINLINE printJob #-}
printJob :: String -> Bool -> IO ()
printJob = unsafePerformIO $ do
p <- newEmptyMVar
return $ \a b -> do
-- here's the function code doing something
-- with variable p, no one else can access.
Как я вижу, различные unsafe*
Нефункции действительно должны использоваться только в тех случаях, когда вы хотите сделать что-то, что уважает ссылочную прозрачность, но реализация которых в противном случае потребовала бы расширения компилятора или системы времени выполнения для добавления новой примитивной возможности. Легче, более модульно, читабельно, легко обслуживаемо и гибко использовать небезопасные вещи, чем модифицировать языковую реализацию для подобных вещей.
Работа FFI часто по своей природе требует от вас подобных действий.
Конечно. Вы можете взглянуть на реальный пример здесь, но в целом, unsafePerformIO
применимо к любой чистой функции, которая оказывается побочным эффектом. IO
Монада все еще может понадобиться для отслеживания эффектов (например, освобождение памяти после вычисления значения), даже когда функция чистая (например, вычисление факториала).
Мне интересно, с какими функциями я могу использовать unsafePerformIO. Я предполагаю, что использование unsafePerformIO с чем-либо, связанным с указателями, - это большая проблема.
Зависит! unsafePerformIO
полностью выполнит действия и вытеснит всю лень, но это не значит, что это сломает вашу программу. В общем, хаскелеры предпочитают unsafePerformIO
появляться только в чистых функциях, так что вы можете использовать его в результатах, например, научных вычислений, но, возможно, не при чтении файлов.