This is a followup question to Is there ever a good reason to use unsafePerformIO?
So we know that
p_sin(double *p) { return sin(*p); }
is unsafe, and cannot be used with unsafePerformIO
.
But the p_sin
function is still a mathematical function, the fact that it was implemented in an unsafe way is an implementation detail. We don't exactly want, say, matrix multiplication to be in IO just because it involves allocating temporary memory.
How can we wrap this function in a safe way? Do we need to lock, allocate memory ourselves, etc? Is there a guide/tutorial for dealing with this?
Actually, if you incorporate the way p_sin
is unsafe from that answer, it depends on p_sin
not being a mathematical function, at least not one from numbers to numbers -- it depends on giving different answers when the memory the same pointer points to is different. So, mathematically speaking, there is something different between the two calls; with a formal model of pointers we might be able to tell. E.g.
type Ptr = Int
type Heap = [Double]
p_sin :: Heap -> Ptr -> Double
and then the C function would be equivalent to
p_sin h p = sin (h !! p)
The reason the results would differ is because of a different Heap
argument, which is unnamed but implicit in the C definition.
If p_sin
used temporary memory internally, but did not depend on the state of memory through its interface, e.g.
double p_sin(double x) {
double* y = (double*)malloc(sizeof(double));
*y = sin(x);
x = *y;
free(y);
return x;
}
then we do have an actual mathematical function Double -> Double
, and we can
foreign import ccall safe "p_sin"
p_sin :: Double -> Double
and we're be fine. Pointers in the interface are killing the purity here, not C functions.
More practically, let's say you have a C matrix multiplication function implemented with pointers, since that's how you model arrays in C. In this case you'd probably expand the abstraction boundary, so there would be a few unsafe things going on in your program, but they would all be hidden from the module user. In this case, I recommend annotating everything unsafe with IO
in your implementation, and then unsafePerformIO
ing right before you give it to the module user. This minimizes the surface area of impurity.
module Matrix
-- only export things guaranteed to interact together purely
(Matrix, makeMatrix, multMatrix)
where
newtype Matrix = Matrix (Ptr Double)
makeMatrix :: [[Double]] -> Matrix
makeMatrix = unsafePerformIO $ ...
foreign import ccall safe "multMatrix"
multMatrix_ :: Ptr Double -> IO (Ptr Double)
multMatrix :: Matrix -> Matrix
multMatrix (Matrix p) = unsafePerformIO $ multMatrix_ p
etc.