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

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

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

Другие вопросы по тегам