Search code examples
javascriptasynchronousffipurescript

Function with Aff callback in FFI


I'm trying to wrap a Javascript library where I have a method A.bar(f) which takes as parameter a function f : B -> void.

Since I'd like to use bar to perform some anynchronous computations, on the Purescript side, I have a function declaration

foreign import foo :: Fn2 A (B -> Aff Unit) -> EffectFnAff Unit

In its corresponding Javascript file, I have something like

exports.foo = function (a, f) {
  return function (onError, onSuccess) {
    a.bar(function (b) {
      f(b)
    })

    return function (cancelError, cancelerError, cancelerSuccess) {
      cancelerSuccess()
    }
  }
}

The issue I have is that f(b) is an Aff object and I don't know how to execute it on the Javascript side.


Solution

  • Accessing PureScript data structures from FFI-ied JavaScript is always a bad idea. You're relying not only on the specific way the library is written (without compiler support to catch errors!), but also on the compiler itself, for the runtime representation may change from one compiler version to another (note that this doesn't apply to EffectFnAff, because it's explicitly intended for FFI and carefully defined as such, in terms of EffectFn2).

    The way to represent an effectful computation in FFI is via Effect:

    foreign import foo :: Fn2 A (B -> Effect Unit) -> EffectFnAff Unit
    

    Such function can now be called from JavaScript the way you do it - as f(b).

    And if you want the consumer of your library to provide Aff, what you do is make a wrapper:

    foreign import foo_ :: Fn2 A (B -> Effect Unit) (EffectFnAff Unit)
    
    foo :: A -> (B -> Aff Unit) -> Aff Unit
    foo a f = fromEffectFnAff $ runFn2 foo_ a (launchAff_ <<< f)
    

    Then you just export the wrapper foo, but not the FFI import foo_.


    On a somewhat related note, I would also recommend doing away with EffectFnAff, because you're not actually launching anything asynchronous, but are always calling cancelerSuccess().

    So instead, I would recommend this:

    // JavaScript
    exports.foo = (a, f) => a.bar(f)
    
    -- PureScript
    foreign import foo_ :: EffectFn2 A (B -> Effect Unit) Unit
    
    foo :: A -> (B -> Aff Unit) -> Aff Unit
    foo a f = liftEffect $ runEffectFn2 foo_ a (launchAff_ <<< f)
    

    The wrapper still has Aff in both plces - this is assuming that you need the whole thing to fit into Aff for your own reasons. Otherwise it could be just foo = runEffectFn2 foo_