FFI-related questions in creating a Wasm API wrapper

(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?

(2) Wrapping a new Error class

The WebAssembly namespace provides several different error classes. So I want to make them distinct from the default Error type. But the useful functions such as throwException and try are currently only available for the Error type.

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.

How do you think about that hack?

1 Like

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.

1 Like

Sorry! I’ll edit the post to fix! (I wonder why I said even though they’re actually typed as Effect…)

1 Like

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

1 Like

Hmmm… Can pure expression be reduced/inlined and optimized at compile time? says currently the compiler doesn’t performs any such optimization. Difficult.

Now I corrected the return type thanks to jy14898’s advice, and to keep it consistent with Effect.Ref.new.
Thank you!