(Side note: I should have posted this in the Feedback category…)
Yesterday I’ve created and made the repository public of a FFI wrapper around the WebAssembly-related API (currently focused on Node’s one).
I’m fluent in Haskell, so learning PureScript wasn’t so hard. But this is my first project in PureScript. I got several questions when writing it:
(1) Effect or pure for a mutable type’s constructor?
I often got troubled about whether some JavaScript functions should be typed as returning a (pure) or Effect a (impure), especially when wrapping a mutable type’s constructor. For example WebAssembly.Table is inherently mutable because it implements the set method. So I marked it as impure. But its constructor doesn’t have any side-effects: it just creates a new Table object. So I marked it as pure.
Is this a right way?
So I made a dirty hack where I typed CompileError as just a record instead of a foreign imported type and made a function to convert an Error value into a CompileError, to cheat the type checker:
I thought this has two advantages over defining a completely separate type from the default Error:
The existing functions (e.g. try) can be used for CompileError without any change.
If the Error were defined as a record, the inheritance by CompileError from Error would be simulated thanks to row polymorphism (i.e. .name and .message are available both in Error and CompileError.) So it’s extensible.
I can’t answer your questions, but I want to note that you should say Effect instead of Eff, which was in an old version of PureScript and was removed. Saying Eff may cause confusion for other people, so please say Effect instead.
To answer your first question, newRaw should be in Effect.
Your side effect is mutation and observing mutation of a ‘reference’ to some Table, and the only way for it to make sense is if that reference is only created once (which we can force via Effect). Eg:
-- This function would ideally always hold `Just val` after evaluation
example :: Foreign -> Effect (Maybe Foreign)
example val = do
let tab = newRaw ...
...
setRaw ... tab val
getRaw ... tab
-- However it can be broken by the compiler inlining tab
-- Notice that now none of the mutations done to the tab would ever carry through
example' :: Foreign -> Effect (Maybe Foreign)
example' val = do
...
setRaw ... (newRaw ...) val
getRaw ... (newRaw ...)
-- If newRaw returns Effect, then the compiler can no longer inline and will only ever perform
-- the creation of the Table once
example'' :: Foreign -> Effect (Maybe Foreign)
example'' val = do
tab <- newRaw ...
...
setRaw ... tab val
getRaw ... tab
EDIT: I have no idea if the compiler would ever actually inline such a thing, as I believe it avoids doing optimisations that change termination behaviour, but the idea is that by using bind you can guarantee your effect is only executed once and in the right order