January 04, 2020

PureScript Async FFI

I recently posted about using Aff for asynchronous effects in PureScript. In this follow-up post, I’m going to discuss using JavaScript FFI in conjuction with Aff .

Aff and FFI

PureScript gives us very nice FFI utilities to call JavaScript code. Aff provides a function called fromEffectFnAff that lets us turn asynchronous JavaScript behavior into an Aff in our PureScript code.

Let’s create an example that reads a file from the file system using FFI. We’ll pass our foreign function a String and expect to get back an Aff String of the contents of the file.

The skeleton of our foreign function will look like this:

exports . _readFile = function ( path ) { return function ( onError , onSuccess ) { readOurFile ( function ( err , res ) { // do things here }); return function ( cancelError , onCancelerError , onCancelerSuccess ) { // handle canceling our Aff here }; }; };

The signature of our foreign function looks like this:

foreign import _readFile :: String -> EffectFnAff String

We receive our path as input and return this complicated looking JavaScript function representing an EffectFnAff . Let’s break down what it does.

First, it receives two arguments, a callback to be called with a successful value and an errback to be called with an error. Next, it performs some asynchronous side effect, usually in a callback. Finally, It returns a function called a “canceler”. If this function is invoked, it’s because the runtime wants to cancel our operation. This takes three arguments. The first tells us the error that is causing the canceling of our operation. The second is a callback to invoke when we have successfully canceled our operation. Last is a callback to invoke if we fail to cancel our operation.

Let’s now fill in the body of our FFI operation and cause it to actually read a file.

var fs = require ( " fs " ); exports . _readFile = function ( path ) { return function ( onError , onSuccess ) { fs . readFile ( path , " utf8 " , function ( err , data ) { if ( err ) { onError ( err ); return ; } onSuccess ( data ); }); return function ( cancelError , onCancelerError , onCancelerSuccess ) { onCancelerSuccess (); }; }; };

Note that our canceler doesn’t do anything. If we’re told to cancel our file read, we just say “yep, we did it!” by calling the onCancelerSuccess function. We attempt to read a file at the provided path and, depending on success or failure, call the callback or the errback. That’s it!

Now let’s look at our PureScript code that uses this JavaScript function.

module File ( readFile , main ) where import Prelude import Effect ( Effect ) import Effect.Aff ( Aff , launchAff_ ) import Effect.Aff.Compat ( EffectFnAff , fromEffectFnAff ) import Effect.Class.Console as Console foreign import _readFile :: String -> EffectFnAff String readFile :: String -> Aff String readFile path = fromEffectFnAff $ _readFile path main :: Effect Unit main = launchAff_ do result <- readFile "./hello.txt" Console . logShow result

We import our foreign function and then use fromEffectFnAff to create an Aff from an EffectFnAff .

Let’s run the code.

$ cat hello.txt hello from drew $ spago run -m File [info] Installation complete. [info] Build succeeded. "hello from drew

"

Aff and Promises

There’s one other way we want to interact with asynchronous JavaScript code – via Promise s. The aff-promise library gives us nice facilities for interacting with JavaScript Promise s and converting them to Aff s. Let’s update our previous JavaScript example to return a Promise .

var fs = require ( " fs " ). promises ; exports . _readFile = function ( path ) { return function () { return fs . readFile ( path , " utf8 " ); }; };

We’re using the new (experimental) promises API to node’s fs module. We’re also returning our Promise inside of a thunk. The reason is that a Promise in JavaScript will begin executing immediately after it is created. To delay this execution until the runtime is ready to perform our Effect , we wrap the Promise in a thunk. Let’s look at the PureScript code.

module Promise ( readFile , main ) where import Prelude import Control.Promise ( Promise , toAffE ) import Effect ( Effect ) import Effect.Aff ( Aff , launchAff_ ) import Effect.Class.Console as Console foreign import _readFile :: String -> Effect ( Promise String ) readFile :: String -> Aff String readFile path = toAffE $ _readFile path main :: Effect Unit main = launchAff_ do result <- readFile "./hello.txt" Console . logShow result

You can see that our foreign import of _readFile returns an Effect (Promise String) , reflecting the fact that we’ve wrapped our Promise in a thunk. We can then use the function toAffE to convert this delayed Promise into an Aff . Here’s the signature of toAffE .

toAffE :: forall a . Effect ( Promise a ) -> Aff a

Exporting Aff back to JavaScript

If we’re building a CommonJS module from our PureScript code that we intend to be consumed by JavaScript code, we can convert an Aff to a Promise for exporting. Here’s an example module.

module ExportAff ( sayHiDelayed , sayHiNow ) where import Prelude import Control.Promise ( Promise , fromAff ) import Effect ( Effect ) import Effect.Aff ( Aff , Milliseconds ( .. ), delay ) import Effect.Aff.Compat ( mkEffectFn1 ) import Effect.Uncurried ( EffectFn1 ) _sayHi :: String -> Aff String _sayHi name = do delay $ Milliseconds 100.0 pure $ "hello " <> name sayHiDelayed :: String -> Effect ( Promise String ) sayHiDelayed name = fromAff $ _sayHi name sayHiNow :: EffectFn1 String ( Promise String ) sayHiNow = mkEffectFn1 sayHiDelayed

This module contains a function _sayHi that takes a name, waits 100 milliseconds and then returns a greeting. We’ve exported this function in two ways.

First, we export an Effect -wrapped Promise . To do this we use the fromAff function.

fromAff :: forall a . Aff a -> Effect ( Promise a )

This works fine, but can feel a bit awkward to consume from the JS side. Here’s an example.

$ spago bundle-module -m ExportAff [info] Installation complete. [info] Build succeeded. [info] Bundling first... [info] Bundle succeeded and output file to index.js [info] Make module succeeded and output file to index.js $ node > var Mod = require("./index"); undefined > Mod.sayHiDelayed("drew"); [Function] > Mod.sayHiDelayed("drew")().then(console.log); Promise { ... } hello drew

We can see that the call to Mod.sayHiDelayed("drew"); returns a [Function] . In practice, this means we need to throw another pair of () on the end of the function to execute the Promise .

The second way we’ve exported this function uses mkEffectFn1 and the EffectFn1 type. These are intended for exposing effectful functions back to JavaScript without required extra () everywhere from the JS consumer. Here’s how it’s used from JavaScript.

> Mod.sayHiNow("drew").then(console.log); Promise { ... } hello drew

Wrap Up