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

23

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

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

Ваш Ответ

6   ответов
12

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

Есть ряд причин, по которым вы можете его использовать. Примеры включают в себя:

  • Initialising global mutable state. (Whether you should ever have such a thing in the first place is a whole other discussion...)

  • Lazy I/O is implemented using this trick. (Again, whether lazy I/O is a good idea in the first place is debatable.)

  • The trace function uses it. (Yet again, it turns out trace is rather less useful than you might imagine.)

  • Perhaps most significantly, you can use it to implement data structures which are referentially transparent, but internally implemented using impure code. Often the ST monad will let you do that, but sometimes you need a little unsafePerformIO.

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

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

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

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

Теперь, чтобы ответить на ваш реальный вопрос: нет особой причины, почему «что-либо без указателей» должно быть нет-нет дляunsafePerformIO. When asking whether to use this function or not, the only question of significance is this: Can the end-user observe any side-effects from doing this?

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

НТН.

21

В конкретном случае FFI,unsafePerformIO предназначен для вызова вещей, которые являются математическими функциями, т.е.solely на входных параметрах, и каждый раз, когда функция вызывается с одинаковыми входами, она будет возвращать один и тот же вывод. Кроме того, у функции не должно быть побочных эффектов, таких как изменение данных на диске или изменение памяти.

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

Error: User Rate Limit ExceededallocaError: User Rate Limit ExceededallocaError: User Rate Limit Exceeded
Error: User Rate Limit ExceededunsafePerformIOError: User Rate Limit Exceededp_sinError: User Rate Limit ExceededunsafePerformIO $ alloca ...Error: User Rate Limit Exceeded
Error: User Rate Limit ExceededpeekCStringError: User Rate Limit ExceededunsafePerformIOError: User Rate Limit Exceeded
unsafePerformIOError: User Rate Limit Exceededpoke p z >> safeSin pError: User Rate Limit ExceededunsafePerformIOError: User Rate Limit ExceededmySin z = unsafePerformIO (poke p z >> safeSin p)Error: User Rate Limit ExceededmySinError: User Rate Limit Exceeded
Error: User Rate Limit ExceededallocaError: User Rate Limit ExceededIORefError: User Rate Limit ExceededallocaError: User Rate Limit ExceededString, unsafePerformIOError: User Rate Limit ExceededCError: User Rate Limit Exceeded
24

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

  1. You know that its use is safe, and

  2. You are unable to prove its safety using the Haskell type system.

Например, вы можете сделать функцию памятки, используя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)

(Это не в моей голове, поэтому я понятия не имею, есть ли в коде грубые ошибки.)

Функция памятки использует и изменяет словарь памятки, но так как функцияas a whole безопасно, вы можете дать ему чистый тип (без использованияIO монада). Тем не менее, вы должны использоватьunsafePerformIO сделать это.

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

Footnote 2: Часто бываютreally subtle ошибки в коде, который используетunsafePerformIOПримером является лишь эскиз возможного использования. Особенно,unsafePerformIO может плохо взаимодействовать с оптимизатором.

Error: User Rate Limit ExceededunsafePerformIOError: User Rate Limit Exceeded
Error: User Rate Limit Exceeded
Error: User Rate Limit ExceededData.MemoUgly
Error: User Rate Limit Exceeded
Error: User Rate Limit ExceededunsafePerformIOError: User Rate Limit Exceeded
5

Стандартный трюк для создания глобальных изменяемых переменных в 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.
Error: User Rate Limit Exceededevil and rude.
Error: User Rate Limit Exceededgithub.com/tkonolige/dbignore/blob/master/ignore.hsError: User Rate Limit Exceeded
0

Конечно. Вы можете взглянуть на реальный примерВот а вообще,unsafePerformIO применимо к любой чистой функции, которая оказывается побочным эффектом.IO монада все еще может быть необходима для отслеживания эффектов (например, освобождение памяти после вычисления значения), даже когда функция чистая (например, вычисление факториала).

I'm wondering what c functions I can use unsafePerformIO with. I assume using unsafePerformIO with anything involving pointers is a big no-no.

Зависит!unsafePerformIO полностью выполнит действия и вытеснит всю лень, но это не означает, что это нарушит вашу программу. В общем, хаскелеры предпочитаютunsafePerformIO появляться только в чистых функциях, так что вы можете использовать его по результатам, например, научные вычисления, но, возможно, не чтение файла.

3

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

Работа FFI часто сама по себе требует от вас подобных действий.

Похожие вопросы