DEV Community

Cover image for Piloting Puppeteer with PureScript - Part 2
Mike Solomon
Mike Solomon

Posted on • Updated on

Piloting Puppeteer with PureScript - Part 2

tl;dr Here's the GitHub repo.

In the previous article in this series, I touted the benefits of using PureScript Aff-s to orchestrate complicated asynchronous workloads like running Puppeteer on AWS serverless.

In this article, I'd like to explore how that code links to the JS layer. PureScript has a straightforward FFI that allows developers to write functions in vanilla JS that are interpreted as strongly-typed functions in the PureScript layer. While nothing prevents an error in typing the JS functions, there is a way to deal with exceptions - using the Effect monad. Let's see how.

Launching a browser

Let's look at the PureScript code for the functions that get the chromium binary and launches the browser. The code is located in Main.purs.

type EPromise a
  = Effect (Promise a)

foreign import executablePath_ :: EPromise String

executablePath = asAff executablePath_ :: Aff String

type LaunchBrowser_
  = String -> EPromise Browser

foreign import launchBrowser_ :: LaunchBrowser_

launchBrowser = asAff launchBrowser_ :: Affize LaunchBrowser_
Enter fullscreen mode Exit fullscreen mode

There are several things going on in those functions, some of which will be the subject of the next articles. In this article, we'll focus just on the foreign declarations and show how they are implemented in the JS layer.

Show me the JS!

The functions that are used to make this possible are in Main.js.

var chromium = require("chrome-aws-lambda");

exports.executablePath_ = function() {
  return chromium.executablePath;
};

exports.launchBrowser_ = function(executablePath) {
  return function() {
    return chromium.puppeteer.launch({
      args: chromium.args,
      defaultViewport: chromium.defaultViewport,
      executablePath: executablePath,
      headless: chromium.headless,
      ignoreHTTPSErrors: true,
    });
  };
};
Enter fullscreen mode Exit fullscreen mode

Specifically, let's compare the two executablePath_-s. The first thing to note is that PureScript returns a function even though executablePath_ is a value of type Effect (Promise String) (shortened to EPromise via the helper type defined above). In PureScript, Effect a needs to be implemented as function () { return a } in JavaScript. This is because the Effect monad always has the possibility of a side effect or error being produced (because JavaScript is unsafe and full of side effects, like writing to the hard drive) and thus wraps it in a function to avoid immediate execution. This allows us to reason about unsafe (impure) code in a safe (pure) way, just like the IO monad in Haskell.

So, there are only two ways we can really mess this up:

  • Returning the wrong type from the JS.
  • Unintentionally provoking an error in the JS.

In PureScript programs, these are the most common sources of bugs, but they are easily spotable because the interface is really clean: it will always be in some module transpiled to a file called foreign.js. However, because the surface area of these JS-interop functions is quite small (usually on the order of a couple lines, like the functions above), the chances there will be a bug are much smaller than a whole program written in JS or TS.

Promises, promises...

In the case of the functions above, they return effectful Promise-s. PureScript has no idea what a Promise is (remember from the last article that we're using Aff-s, so it treats the promise like an opaque blog and happily passes it to the next consumer, which marshals it into an Aff.

In the next article, I'll talk about how the type system can enforce the use of a consistent asynchronous context, namely Aff, so that we don't have to juggle promises, callbacks, async/await and vanilla code.

Top comments (0)