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\n"
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
I hope these examples give you an idea of how Aff
can be used in both
PureScript and JavaScript applications that require FFI. The tools described
above should handle 90% of your Aff-based FFI use cases.