Подтвердить что ты не робот

Есть ли веская причина использовать unsafePerformIO?

В этом вопросе говорится все. Более конкретно, я пишу привязки к библиотеке C, и мне интересно, с какими функциями c я могу использовать unsafePerformIO с. Я предполагаю, что использование unsafePerformIO с чем-либо, содержащим указатели, является большим, не-no.

Было бы здорово увидеть другие случаи, когда допустимо использовать также unsafePerformIO.

4b9b3361

Ответ 1

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

Ответ 2

Не нужно включать здесь C. Функция unsafePerformIO может использоваться в любой ситуации, где

  • Вы знаете, что его использование безопасно, и

  • Вы не можете доказать свою безопасность, используя систему типов Haskell.

Например, вы можете сделать функцию memoize с помощью 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 использует и изменяет словарь memoization, но поскольку функция в целом безопасна, вы можете дать ей чистый тип (без использования монады IO). Однако для этого нужно использовать unsafePerformIO.

Сноска: Когда дело доходит до FFI, вы несете ответственность за предоставление типов функций C системе Haskell. Вы можете добиться эффекта unsafePerformIO, просто опуская IO из типа. Система FFI по своей сути небезопасна, поэтому использование unsafePerformIO не имеет большого значения.

Сноска 2: В коде, который использует unsafePerformIO, часто очень тонкие ошибки, это пример эскиза возможного использования. В частности, unsafePerformIO может плохо взаимодействовать с оптимизатором.

Ответ 3

Очевидно, что если он никогда не будет использоваться, он не будет в стандартных библиотеках.; -)

Существует несколько причин, по которым вы можете использовать его. Примеры включают:

  • Инициализация глобального изменчивого состояния. (Если вы когда-либо должны иметь такую ​​вещь, в первую очередь, это еще одно обсуждение...)

  • Lazy I/O реализуется с использованием этого трюка. (Опять же, является ли ленивый ввод-вывод хорошей идеей в первую очередь спорным.)

  • Функция trace использует его. (Опять же, оказывается, что trace менее полезен, чем вы могли себе представить.)

  • Возможно, что наиболее важно, вы можете использовать его для реализации структур данных, которые являются ссылочно прозрачными, но внутренне реализованы с использованием нечистого кода. Часто монада ST позволит вам это сделать, но иногда вам нужно немного unsafePerformIO.

Ленивый ввод-вывод можно рассматривать как особый случай последней точки. Так может быть memoisation.

Рассмотрим, например, "неизменный", растущий массив. Внутренне вы можете реализовать это как чистый "дескриптор", который указывает на изменяемый массив. Ручка содержит видимый пользователем размер массива, но фактический основной изменчивый массив больше этого. Когда пользователь "присоединяется" к массиву, возвращается новый дескриптор с новым большим размером, но добавление выполняется путем изменения основного изменчивого массива.

Вы не можете сделать это с помощью монады ST. (Вернее, вы можете, но он все еще требует unsafePerformIO.)

Обратите внимание, что это чертовски сложно понять. И проверка типа не поймает, если вы ошибаетесь. (Что делает unsafePerformIO, это делает проверку типов не проверкой, что вы делаете это правильно!) Например, если вы добавляете к "старому" дескриптору, правильная вещь, которую нужно сделать, это скопировать базовый изменяемый массив, Забудьте об этом, и ваш код будет вести себя очень странно.

Теперь, чтобы ответить на ваш реальный вопрос: нет никакой особой причины, почему "ничего с указателями" должно быть no-no для unsafePerformIO. Когда вы спрашиваете, следует ли использовать эту функцию или нет, остается только вопрос значимости: может ли конечный пользователь наблюдать за любыми побочными эффектами от этого?

Если единственное, что он делает, это создать некоторый буфер где-нибудь, что пользователь не может "видеть" из чистого кода, это прекрасно. Если он записывает файл на диск... не так хорошо.

НТН.

Ответ 4

Стандартный трюк для создания глобальных переменных переменных в 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 the function code doing something 
              -- with variable p, no one else can access.

Ответ 5

Как я вижу, различные функции unsafe* действительно должны использоваться только в тех случаях, когда вы хотите сделать что-то, что касается ссылочной прозрачности, но для реализации которого в противном случае потребовалось бы расширение системы компилятора или системы выполнения, чтобы добавить новую примитивную возможность, Это проще, более модульно, легко читаемо, удобно и гибко использовать небезопасные вещи, чем для изменения языковой реализации для таких вещей.

Работа FFI часто по своей сути требует, чтобы вы делали такие вещи.