質問はそれをすべて言います。より具体的には、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 関数はメモ化辞書を使用して変更しますが、関数全体IO
として安全であるため、(モナドを使用せずに) 純粋な型を指定できます。ただし、そのためには使用unsafePerformIO
する必要があります。
脚注: FFI に関しては、C 関数の型を Haskell システムに提供する責任があります。タイプからunsafePerformIO
単に省略することで、 の効果を得ることができます。IO
FFI システムは本質的に安全でunsafePerformIO
はないため、使用しても大きな違いはありません。
脚注 2:を使用するコードには、非常に微妙なバグがしばしばありunsafePerformIO
ます。特に、unsafePerformIO
オプティマイザとの対話が不十分になる可能性があります。
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
、プログラムは式の再評価を強制されるため、代わりに更新されたポインター データが使用されます。
明らかに、使用されるべきではない場合、標準ライブラリにはありません。;-)
使用する理由はいくつかあります。例は次のとおりです。
グローバル可変状態を初期化しています。(そもそもそのようなことをすべきかどうかは、まったく別の議論です...)
遅延 I/O は、このトリックを使用して実装されます。(繰り返しになりますが、そもそも遅延 I/O が良いアイデアであるかどうかは議論の余地があります。)
trace
関数はそれを使用します。(繰り返しになりtrace
ますが、想像以上に役に立たないことがわかりました。)
おそらく最も重要なのは、それを使用して、参照透過的であるが、内部的には純粋でないコードを使用して実装されたデータ構造を実装できることです。多くの場合、ST
モナドがそれを可能にしますが、時には少しunsafePerformIO
.
レイジー I/O は、最後のポイントの特殊なケースと見なすことができます。メモ化も同様です。
たとえば、「不変」で拡張可能な配列を考えてみましょう。内部的には、可変配列を指す純粋な「ハンドル」として実装できます。ハンドルはユーザーに見える配列のサイズを保持しますが、実際の基礎となる可変配列はそれよりも大きくなります。ユーザーが配列に「追加」すると、新しい大きなサイズの新しいハンドルが返されますが、追加は基になる可変配列を変更することによって実行されます。
ST
これはモナドではできません。(というか、できますが、それでも必要unsafePerformIO
です。)
この種のことを正しく行うのは非常に難しいことに注意してください。間違っていると、型チェッカーはキャッチしません。(これが原因unsafePerformIO
です。タイプ チェッカーは、正しく実行されているかどうかをチェックしません!) たとえば、「古い」ハンドルに追加する場合、正しいことは、基になる可変配列をコピーすることです。これを忘れると、コードは非常に奇妙な動作をします。
さて、あなたの本当の質問に答えるために:「ポインターのないもの」がunsafePerformIO
. この関数を使用するかどうかを尋ねるとき、重要な唯一の問題は次のとおりです。エンドユーザーはこれを行うことによる副作用を観察できますか?
ユーザーが純粋なコードから「見る」ことができない場所にバッファを作成するだけであれば、それで問題ありません。ディスク上のファイルに書き込む場合...それほど問題ありません。
HTH。
私の見方では、さまざまなunsafe*
非関数は、参照透過性を尊重する何かを実行したいが、そうでなければコンパイラまたはランタイムシステムを拡張して新しいプリミティブ機能を追加する必要がある場合にのみ使用する必要があります。そのようなもののために言語実装を変更するよりも、安全でないものを使用する方が簡単で、よりモジュール化され、読みやすく、保守可能で、機敏です。
FFI の作業では、多くの場合、本質的にこの種のことを行う必要があります。