问题就是这一切.更具体地说,我正在编写绑定到C库,我想知道我可以使用哪些c函数unsafePerformIO.我假设使用unsafePerformIO任何涉及指针的东西是一个很大的禁忌.
很高兴看到其他情况也可以使用unsafePerformIO.
Die*_*Epp 24
这里不需要涉及C. 该unsafePerformIO功能可用于任何情况,
你知道它的使用是安全的,并且
您无法使用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与优化器的交互能力很差.
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中,程序被强制重新计算表达式,因此使用更新的指针数据.
Mat*_*hid 12
显然,如果它永远不会被使用,它就不会出现在标准库中.;-)
您可以使用它的原因有很多.例子包括:
初始化全局可变状态.(你是否应该首先拥有这样的东西是另一个讨论...)
使用此技巧实现了懒惰I/O. (同样,首先,懒惰的I/O是否是一个好主意是值得商榷的.)
该trace功能使用它.(然而,事实证明,它并trace没有你想象的那么有用.)
也许最重要的是,您可以使用它来实现引用透明的数据结构,但使用不纯的代码在内部实现.STmonad 通常会让你这样做,但有时你需要一点点unsafePerformIO.
懒惰的I/O可以被视为最后一点的特例.所以可以回忆.
例如,考虑一个"不可变"的可增长数组.在内部,您可以将其实现为指向可变数组的纯"句柄" .句柄保存数组的用户可见大小,但实际的底层可变数组大于此值.当用户"追加"到数组时,将返回一个新的,更大的新句柄,但是通过改变底层的可变数组来执行追加.
你不能用STmonad 做到这一点.(或者更确切地说,你可以,但它仍然需要unsafePerformIO.)
请注意,让这种事情正确起来真是太棘手了.如果你错了,类型检查器将无法捕获.(这是unsafePerformIO做什么的;它使类型检查器不检查你正确地执行它!)例如,如果你附加到"旧"句柄,正确的做法是复制底层的可变数组.忘记这一点,你的代码将表现得非常奇怪.
现在,回答你真正的问题:没有什么特别的理由为什么"任何有指针的东西"都应该是禁忌unsafePerformIO.在询问是否使用此功能时,唯一重要的问题是:最终用户是否可以观察到这样做的副作用?
如果它唯一能做的就是在用户无法从纯代码中"看到"某处创建一些缓冲区,那很好.如果它写入磁盘上的文件...不太好.
HTH.
在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)