这个问题说明了一切。更具体地说,我正在编写与 C 库的绑定,我想知道我可以使用哪些 c 函数unsafePerformIO
。我认为unsafePerformIO
与任何涉及指针的东西一起使用是一个很大的禁忌。
很高兴看到其他也可以使用的情况unsafePerformIO
。
这个问题说明了一切。更具体地说,我正在编写与 C 库的绑定,我想知道我可以使用哪些 c 函数unsafePerformIO
。我认为unsafePerformIO
与任何涉及指针的东西一起使用是一个很大的禁忌。
很高兴看到其他也可以使用的情况unsafePerformIO
。
此处无需涉及 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
monad)。但是,您必须使用它unsafePerformIO
来执行此操作。
脚注:当涉及到 FFI 时,您负责向 Haskell 系统提供 C 函数的类型。您可以通过简单地从类型中unsafePerformIO
省略来实现的效果。IO
FFI 系统本质上是不安全的,因此使用unsafePerformIO
并没有太大的区别。
脚注 2:使用 的代码中通常存在非常细微的错误unsafePerformIO
,该示例只是可能使用的草图。特别是,unsafePerformIO
与优化器的交互可能很差。
在 FFI 的特定情况下,unsafePerformIO
用于调用数学函数,即输出仅取决于输入参数,并且每次使用相同的输入调用函数时,它都会返回相同的输出。此外,该函数不应有副作用,例如修改磁盘上的数据或改变内存。
例如,大多数函数<math.h>
都可以用 调用unsafePerformIO
。
你是对的unsafePerformIO
,指针通常不会混合。例如,假设您有
p_sin(double *p) { return sin(*p); }
即使您只是从指针中读取值,使用unsafePerformIO
. 如果你 wrap 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 中,程序被迫重新计算表达式,因此使用更新后的指针数据。
显然,如果它永远不应该被使用,它就不会出现在标准库中。;-)
您可能会使用它的原因有很多。示例包括:
初始化全局可变状态。(你是否应该首先拥有这样的东西是另一个完全不同的讨论......)
延迟 I/O 就是使用这个技巧来实现的。(同样,惰性 I/O 是否是一个好主意首先是值得商榷的。)
该trace
函数使用它。(再一次,事实证明它trace
没有你想象的那么有用。)
也许最重要的是,您可以使用它来实现引用透明但内部使用不纯代码实现的数据结构。通常ST
monad 会让你这样做,但有时你需要一点unsafePerformIO
.
Lazy I/O 可以看作是最后一点的特例。记忆也可以。
例如,考虑一个“不可变的”、可增长的数组。在内部,您可以将其实现为指向可变数组的纯“句柄”。句柄保存数组的用户可见大小,但实际的底层可变数组大于此大小。当用户“追加”到数组时,会返回一个新的句柄,具有新的更大的大小,但追加是通过改变底层可变数组来执行的。
你不能用ST
monad 做到这一点。(或者更确切地说,你可以,但它仍然需要unsafePerformIO
。)
请注意,要正确处理这类事情是非常棘手的。如果你错了,类型检查器也不会发现。(就是这样unsafePerformIO
做的;它使类型检查器无法检查您是否正确执行此操作!)例如,如果您附加到“旧”句柄,则正确的做法是复制底层的可变数组。忘记这一点,你的代码会表现得很奇怪。
现在,回答你真正的问题:没有特别的理由为什么“没有指针的任何东西”应该是禁止的unsafePerformIO
。当询问是否使用此功能时,唯一有意义的问题是:最终用户可以观察到这样做的任何副作用吗?
如果它唯一要做的就是在某个用户无法从纯代码中“看到”的地方创建一些缓冲区,那很好。如果它写入磁盘上的文件......不太好。
HTH。
在 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
即使函数是纯函数(例如计算阶乘),monad 仍可能需要跟踪效果(例如,在计算值后释放内存)。
我想知道我可以将 unsafePerformIO 与哪些 c 函数一起使用。我认为将 unsafePerformIO 与涉及指针的任何内容一起使用是一个很大的禁忌。
依靠!unsafePerformIO
将完全执行操作并消除所有惰性,但这并不意味着它会破坏您的程序。一般来说,Haskellers 更喜欢unsafePerformIO
只出现在纯函数中,因此您可以将它用于例如科学计算的结果,但可能不能用于文件读取。