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

时间:2012-05-10 07:27:28

标签: haskell

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

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

6 个答案:

答案 0 :(得分:24)

这里不需要涉及C. unsafePerformIO函数可以在任何情况下使用,

  1. 您知道它的使用是安全的,

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

  3. 例如,您可以使用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)
    

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

    memoize函数使用和修改memoization字典,但由于函数作为一个整体是安全的,你可以给它一个纯类型(不使用IO monad) 。但是,您必须使用unsafePerformIO来执行此操作。

    脚注:说到FFI,您负责向Haskell系统提供C函数的类型。只需从类型中省略unsafePerformIO即可实现IO的效果。 FFI系统本质上是不安全的,因此使用unsafePerformIO并没有太大的区别。

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

答案 1 :(得分:21)

在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中,程序被强制重新计算表达式,因此使用更新的指针数据。

答案 2 :(得分:12)

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

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

  • 初始化全局可变状态。 (你是否应该首先拥有这样的事情是一个完整的其他讨论......)

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

  • trace函数使用它。 (然而,事实证明trace没有你想象的那么有用。)

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

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

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

你无法使用ST monad执行此操作。 (或者更确切地说,你可以,但它仍然需要unsafePerformIO。)

请注意,让这种事情变得正确是件非常棘手的。如果你错了,类型检查器将无法捕获。 (unsafePerformIO的作用是什么;它使类型检查器不检查您是否正确执行它!)例如,如果您附加到“旧”句柄,正确的做法是复制基础可变数组。忘记这一点,你的代码将非常奇怪地。

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

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

HTH。

答案 3 :(得分:6)

在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.

答案 4 :(得分:3)

我看到它的方式,各种unsafe*非函数实际上只应该用于你想要做一些尊重参考透明度的事情但是其实现需要增加编译器或运行时系统来添加新的原始能力。使用不安全的东西比修改类似的语言实现更容易,更模块化,可读性,可维护性和敏捷性。

FFI工作通常本质上要求你做这类事情。

答案 5 :(得分:0)

不确定。您可以查看一个真实示例here,但一般情况下,unsafePerformIO可用于任何恰好有副作用的纯函数。即使在函数是纯的(例如,计算因子)时,仍然可能需要IO monad来跟踪效果(例如,在计算值之后释放存储器)。

  

我想知道我可以使用unsafePerformIO的c函数。我假设使用unsafePerformIO与任何涉及指针的东西是一个很大的禁忌。

取决于! unsafePerformIO将完全执行操作并强制执行所有懒惰,但这并不意味着它会破坏您的程序。一般来说,Haskellers希望unsafePerformIO仅出现在纯函数中,因此您可以将其用于例如科学计算,但可能不是文件读取。