是否有充分的理由使用unsafePerformIO?

Vla*_*ala 25 haskell

问题就是这一切.更具体地说,我正在编写绑定到C库,我想知道我可以使用哪些c函数unsafePerformIO.我假设使用unsafePerformIO任何涉及指针的东西是一个很大的禁忌.

很高兴看到其他情况也可以使用unsafePerformIO.

Die*_*Epp 24

这里不需要涉及C. 该unsafePerformIO功能可用于任何情况,

  1. 你知道它的使用是安全的,并且

  2. 您无法使用Haskell类型系统证明其安全性.

例如,您可以使用unsafePerformIO以下命令创建memoize函数:

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)
Run Code Online (Sandbox Code Playgroud)

(这是我的头脑,所以我不知道代码中是否存在明显的错误.)

该memoize的功能使用和修改了记忆化字典,但由于功能作为一个整体是安全的,你可以给它一个纯粹的类型(没有使用的IO单子).但是,你必须unsafePerformIO这样做.

脚注:当涉及到FFI时,您负责向Haskell系统提供C函数的类型.您可以unsafePerformIO通过简单地省略IO类型来实现效果.FFI系统本质上是不安全的,因此使用unsafePerformIO并没有太大的区别.

脚注2:代码中经常存在微妙的错误unsafePerformIO,示例只是可能用途的草图.特别是,unsafePerformIO与优化器的交互能力很差.

  • 我认为,如果您要准确指定程序员如何知道“unsafePerformIO”是安全的,这个答案会更加有力。 (4认同)
  • @JohnL:如果我们对这个问题有一个确切的答案,我们就能够建立一个完美的类型系统.一般来说问题是棘手的. (2认同)
  • @DietrichEpp:当然,但是程序员可以对一段代码有所了解,这些代码无法对所有代码片段进行一般性的证明.例如,我可以知道特定程序是否会停止,即使给定的系统无法证明我是对的.您的答案没有给出任何"安全"的定义,也没有给出程序员在做出此决定时可能使用的"安全"含义的任何信息. (2认同)
  • @ruakh:这正是我的观点:如果程序员知道一个特定的程序是正确的,即使它的正确性不能被给定的系统(Haskell 类型系统)证明,那么你可以使用 `unsafePerformIO`。对“安全”的含义的处理超出了本答案的范围,如果您想了解更多关于类型安全的基础知识,请随时提出问题。 (2认同)

Joh*_*n L 21

在FFI的特定情况下,unsafePerformIO意味着用于调用数学函数,即输出取决于输入参数,并且每次使用相同的输入调用函数时,它将返回相同的输出.此外,该功能不应具有副作用,例如修改磁盘上的数据或改变内存.

例如,<math.h>可以调用大多数函数unsafePerformIO.

你是正确的,unsafePerformIO指针通常不会混合.例如,假设你有

p_sin(double *p) { return sin(*p); }
Run Code Online (Sandbox Code Playgroud)

即使你只是从指针读取一个值,它也不安全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
Run Code Online (Sandbox Code Playgroud)

编译时,该程序输出

$ ./main 
unsafe: 0.0
safe: 0.0
unsafe: 0.0
safe: 0.8414709848078965
Run Code Online (Sandbox Code Playgroud)

即使指针引用的值在对"sin1"的两个引用之间发生了更改,也不会重新计算表达式,从而导致使用过时数据.由于safeSin(因此sin2)在IO中,程序被强制重新计算表达式,因此使用更新的指针数据.

  • @ imz - IvanZakharyaschev你是对的,由于`p_sin`的类型签名而隐式添加`unsafePerformIO`(不确定GHC是否仍然允许).您的方法没问题,但如果多个线程使用相同的指针,则它不是线程安全的.您可以将该操作填充到alloca中,例如`unsafePerformIO $ alloca ...`,这应该是完全安全的. (3认同)
  • 您的答案中没有明确提及“unsafePerformIO”。如果我将动作序列“poke pz &gt;&gt; safeSin p”包装在“unsafePerformIO”中,如下所示:“mySin z = unsafePerformIO (poke pz &gt;&gt; safeSin p)”,然后使用“mySin”作为普通函数,我应该没事吧? (2认同)

Mat*_*hid 12

显然,如果它永远不会被使用,它就不会出现在标准库中.;-)

您可以使用它的原因有很多.例子包括:

  • 初始化全局可变状态.(你是否应该首先拥有这样的东西是另一个讨论...)

  • 使用此技巧实现了懒惰I/O. (同样,首先,懒惰的I/O是否是一个好主意是值得商榷的.)

  • trace功能使用它.(然而,事实证明,它并trace没有你想象的那么有用.)

  • 也许最重要的是,您可以使用它来实现引用透明的数据结构,但使用不纯的代码在内部实现.STmonad 通常会让你这样做,但有时你需要一点点unsafePerformIO.

懒惰的I/O可以被视为最后一点的特例.所以可以回忆.

例如,考虑一个"不可变"的可增长数组.在内部,您可以将其实现为指向可变数组的纯"句柄" .句柄保存数组的用户可见大小,但实际的底层可变数组大于此值.当用户"追加"到数组时,将返回一个新的,更大的新句柄,但是通过改变底层的可变数组来执行追加.

你不能用STmonad 做到这一点.(或者更确切地说,你可以,但它仍然需要unsafePerformIO.)

请注意,让这种事情正确起来真是太棘手了.如果你错了,类型检查器将无法捕获.(这是unsafePerformIO做什么的;它使类型检查器不检查你正确地执行它!)例如,如果你附加到"旧"句柄,正确的做法是复制底层的可变数组.忘记这一点,你的代码将表现得非常奇怪.

现在,回答你真正的问题:没有什么特别的理由为什么"任何有指针的东西"都应该是禁忌unsafePerformIO.在询问是否使用此功能时,唯一重要的问题是:最终用户是否可以观察到这样做的副作用?

如果它唯一能做的就是在用户无法从纯代码中"看到"某处创建一些缓冲区,那很好.如果它写入磁盘上的文件...不太好.

HTH.

  • "unsafePerformIO"和"unsafeInterleaveIO"对IO的性能都不严格.区别在于,使用`unsafeInterleaveIO`,结果保留在IO中(根据类型系统).使用常规严格的"IO",类型清楚地表明IO正在执行,以及何时发生.对于`unsafeInterleaveIO`,类型仍然表示IO正在发生,但不会发生.使用`unsafePerformIO`,您不仅不知道何时发生IO,您甚至不知道它正在发生. (4认同)
  • 惰性I/O没有用`unsafePerformIO`实现,它使用`unsafeInterleaveIO`,这是一个完全不同(且不太安全)的函数. (2认同)
  • 请参阅维基上的"IO Inside"页面和Magnus Therning的博客文章.这些可能是最好的资源(虽然我承认仍然很少).http://www.haskell.org/haskellwiki/IO_inside http://therning.org/magnus/archives/249 (2认同)

Vag*_*rdi 6

在haskell中实例化全局可变变量的标准技巧:

{-# NOINLINE bla #-}
bla :: IORef Int
bla = unsafePerformIO (newIORef 10)
Run Code Online (Sandbox Code Playgroud)

如果我想阻止在我提供的函数之外访问它,我也用它来关闭全局变量:

{-# 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.
Run Code Online (Sandbox Code Playgroud)