33

質問はそれをすべて言います。より具体的には、C ライブラリへのバインドを作成していますが、どの C 関数を使用できるのか疑問に思っていますunsafePerformIOunsafePerformIOポインターを含むものを使用することは、絶対にダメだと思います。

使用が許容される他のケースも見られるとよいでしょうunsafePerformIO

4

6 に答える 6

27

ここで C を使用する必要はありません。このunsafePerformIO機能は、次のようなあらゆる状況で使用できます。

  1. あなたはその使用が安全であることを知っており、

  2. 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 関数はメモ化辞書を使用して変更しますが、関数全体IOとして安全であるため、(モナドを使用せずに) 純粋な型を指定できます。ただし、そのためには使用unsafePerformIOする必要があります。

脚注: FFI に関しては、C 関数の型を Haskell システムに提供する責任があります。タイプからunsafePerformIO単に省略することで、 の効果を得ることができます。IOFFI システムは本質的に安全でunsafePerformIOはないため、使用しても大きな違いはありません。

脚注 2:を使用するコードには、非常に微妙なバグがしばしばありunsafePerformIOます。特に、unsafePerformIOオプティマイザとの対話が不十分になる可能性があります。

于 2012-05-10T07:49:45.800 に答える
24

FFI の特定のケースでは、unsafePerformIO数学関数を呼び出すために使用することを意図しています。つまり、出力は入力パラメーターのみに依存し、関数が同じ入力で呼び出されるたびに、同じ出力が返されます。また、関数には、ディスク上のデータの変更やメモリの変更などの副作用があってはなりません。

のほとんどの関数は、たとえば<math.h>で呼び出すことができます。unsafePerformIO

あなたは正しいですしunsafePerformIO、ポインターは通常混在しません。たとえば、あなたが持っているとします

p_sin(double *p) { return sin(*p); }

ポインターから値を読み取っているだけでも、安全に使用できませんunsafePerformIO。をラップp_sinすると、複数の呼び出しでポインター引数を使用できますが、異なる結果が得られます。IOポインターの更新に関して適切に順序付けされるようにするには、関数を保持する必要があります。

この例は、これが安全でない理由の 1 つを明らかにする必要があります。

# 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" への 2 つの参照の間で変更されていても、式は再評価されないため、古いデータが使用されます。safeSin(したがって) は IO にあるためsin2、プログラムは式の再評価を強制されるため、代わりに更新されたポインター データが使用されます。

于 2012-05-10T09:15:32.520 に答える
15

明らかに、使用されるべきではない場合、標準ライブラリにはありません。;-)

使用する理由はいくつかあります。例は次のとおりです。

  • グローバル可変状態を初期化しています。(そもそもそのようなことをすべきかどうかは、まったく議論です...)

  • 遅延 I/O は、このトリックを使用して実装されます。(繰り返しになりますが、そもそも遅延 I/O が良いアイデアであるかどうかは議論の余地があります。)

  • trace関数はそれを使用します。(繰り返しになりtraceますが、想像以上に役に立たないことがわかりました。)

  • おそらく最も重要なのは、それを使用して、参照透過的であるが、内部的には純粋でないコードを使用して実装されたデータ構造を実装できることです。多くの場合、STモナドがそれを可能にしますが、時には少しunsafePerformIO.

レイジー I/O は、最後のポイントの特殊なケースと見なすことができます。メモ化も同様です。

たとえば、「不変」で拡張可能な配列を考えてみましょう。内部的には、可変配列を指す純粋な「ハンドル」として実装できます。ハンドルはユーザーに見える配列のサイズを保持しますが、実際の基礎となる可変配列はそれよりも大きくなります。ユーザーが配列に「追加」すると、新しい大きなサイズの新しいハンドルが返されますが、追加は基になる可変配列を変更することによって実行されます。

STこれはモナドではできません。(というか、できますが、それでも必要unsafePerformIOです。)

この種のことを正しく行うのは非常に難しいことに注意してください。間違っていると、型チェッカーはキャッチしません。(これが原因unsafePerformIOです。タイプ チェッカーは、正しく実行されているかどうかをチェックしません!) たとえば、「古い」ハンドルに追加する場合、正しいことは、基になる可変配列をコピーすることです。これを忘れると、コードは非常に奇妙な動作をします。

さて、あなたの本当の質問に答えるために:「ポインターのないもの」がunsafePerformIO. この関数を使用するかどうかを尋ねるとき、重要な唯一の問題は次のとおりです。エンドユーザーはこれを行うことによる副作用を観察できますか?

ユーザーが純粋なコードから「見る」ことができない場所にバッファを作成するだけであれば、それで問題ありません。ディスク上のファイルに書き込む場合...それほど問題ありません。

HTH。

于 2012-05-10T08:48:25.333 に答える
4

私の見方では、さまざまなunsafe*非関数は、参照透過性を尊重する何かを実行したいが、そうでなければコンパイラまたはランタイムシステムを拡張して新しいプリミティブ機能を追加する必要がある場合にのみ使用する必要があります。そのようなもののために言語実装を変更するよりも、安全でないものを使用する方が簡単で、よりモジュール化され、読みやすく、保守可能で、機敏です。

FFI の作業では、多くの場合、本質的にこの種のことを行う必要があります。

于 2012-05-10T17:20:34.360 に答える