Using PureScript in an impure way

Hello functional programmers,
a trivial, maybe common, probably blasphemous question.

Is it possible to declare FFI functions with side effects (i.e. IO functions or mutations) without using effects/monads?

I’m wondering if we can use PureScript in a way similar to Reason, Gleam, Grain, F# and so on. Purity is wonderful but sometimes we just don’t need it, yet we could still take advantage of the great PureScript type system.

I suppose that the compiler, assuming referential transparency, could rewrite the transpiled code in an unexpected way, producing a wrong program. In such case is there a way to disable these optimizations?

I’m mainly interested to the Purerl project, I would like to have the option to write Erlang code in type safe manner without having an huge cognitive effort.

Thanks in advance.

1 Like

I didn’t really give much thought to this, but couldn’t you achieve this by wrapping basically everything in Effect?

(Yeah, I realize that might be impractical, but I’m just saying my thoughts out loud here.)

Without a value restriction, it is pretty easy to subvert the type system using polymorphism and mutability (which can crop up in unintuitive ways), so an “impure” subset is not sound. We use Effect and bind as an alternative to the value restriction. Whether or not this is important to you, I can’t say, and of course there’s already unsafeCoerce. I personally would not like to see this style of programming in PureScript become something that is advertised or pushed by a subset of the community.

9 Likes

There’s nothing that technically prevents you from doing this. In some cases there are libraries which do this in a limited way, for example by giving internal functions dodgy types which, say, claim to be pure when they are not. Importantly, though, those functions mostly aren’t exposed so the actual API surface is not making any dodgy claims.

There are no such optimizations that I am aware of off the top of my head which would invalidate this approach, but that’s not to say you’ll be without problems if you write code this way. I think your two main problems with writing PureScript this way are going to be:
a) everyone expects you to write PureScript the normal way, which is going to add friction whenever you are using 3rd party libraries
b) without purity, evaluation order becomes much more important, and it’s sometimes difficult to predict what will be evaluated when, especially when you have lots of where and let involved. An unused binding in a where clause could quite easily result in your code triggering a request to some API unconditionally even if you only intended it to happen on a certain code path, for example.

6 Likes

The one thing i would say in defence of ignoring side-effects and hidden polymorphism is that it can be useful (i think) as a transitionary stage while prototyping how you actually want to wrap some library or JS functionality. Taking a very principled approach from the get-go can - in my experience anyway - lead to a lot of busy-work on the type signatures of the foreign functions.

But i absolutely agree that it’s self-defeating to use PureScript this way and likely something you’ll regret later if you expose such aberrations to other people or other code. It will bite you eventually and it will be hard to debug.

1 Like

Thanks for all your feedback.
I tried but this approach seems more tricky than using PureScript in the right way.
So I agree with you, it’s a bad idea.

1 Like

I am aware of off the top of my head which would invalidate this approach, but that’s not to say you’ll be without problems if you write code this way.

For those curious about why this is unsafe, the Haskell documentation for unsafePerformIO has a note:

https://www.stackage.org/haddock/lts-15.8/base-4.13.0.0/System-IO-Unsafe.html#v:unsafePerformIO

…and if you’re curious about the value restriction, which is used to avoid this issue in non-pure languages, communities like F# and OCaml have plenty of writing about it. This is a particularly good dive into why this restriction exists in OCaml:

https://ocaml.org/manual/polymorphism.html

1 Like

I think it probably is worth noting that many of the issues mentioned for the Haskell function unsafePerformIO don’t apply to PureScript. For example, the PureScript compiler doesn’t have a general purpose inliner, and there shouldn’t be issues around operation ordering either (at least not when compiling to JavaScript), because JavaScript does guarantee that statements happen in the order you write them as far as I know. Also, the PureScript compiler doesn’t implement common subexpression elimination. Then again, the compiler might start doing these things at some point in the future, at which point code which uses unsafePerformEffect might start misbehaving for the same reasons.

That’s a good point, and my use of that link was probably not a good reference. I was mostly thinking of the last section, which reads:

It is less well known that unsafePerformIO is not type safe. For example:

test :: IORef [a]
test = unsafePerformIO $ newIORef []

main = do
        writeIORef test [42]
        bang <- readIORef test
        print (bang :: [Char])

This program will core dump. This problem with polymorphic references is well known in the ML community, and does not arise with normal monadic use of references. There is no easy way to make it impossible once you use unsafePerformIO. Indeed, it is possible to write coerce :: a → b with the help of unsafePerformIO. So be careful!

4 Likes