LoginSignup
8
4

More than 5 years have passed since last update.

User empowerment of FFI in PureScript

Last updated at Posted at 2018-07-29

When using PureScript, one of the main features is being able to use the foreign function interface for directly calling into JavaScript to get the results you need. This has many benefits, such as

  • Being able to give proper types to a given expression
  • Not being limited by runtime implementations for when certain kinds of operations can be performed, especially concerning synchronous vs asynchronous, privileged callbacks, etc.
  • Being able to build on top of the FFI imports
  • Being able to do things some implementer/"thoughtleader" hasn't thought of doing

So instead of getting stuck when someone can't find an existing solution for something that requires calls to JavaScript that a) exists, b) is useful for them, or c) that works well enough, it's much more useful to enable them to help themselves. And so, I hope to provide some real examples in this post with some links to resources where people can actually learn more.

Working with Puppeteer

Puppeteer is a Node library for working with Chrome/Chromium over the DevTools protocol, so that you can automate browser actions for doing things like poking around at your application, scraping image search results, and other things.

To work with Puppeteer with a sensible typed interface in PureScript, I made the library Toppokki. For example, to have Puppeteer open example.com, test that the content is not empty, and take a screenshot and PDF, I can work with the following code:

import Toppokki as T
main = launchAff_ do
  browser <- T.launch

  page <- T.newPage browser
  T.goto (T.URL "https://example.com") page

  content <- T.content page
  Assert.assert "content is non-empty string" (String.length content > 0)

  _ <- T.screenshot {path: "./test/test.png"} page
  _ <- T.pdf {path: "./test/test.pdf"} page

  T.close browser

To implement this library or something similar in your codebase, you really only need to see these three resources:

Primarily, I'll cover three main things that need to be known to work with FFI effectively in PureScript:

  • Declaring a foreign data type
  • Uncurried pure and Effect functions
  • Aff-Promise to thunk Promises and use Aff

Declaring a foreign data type

If you have some value that should be typed but cannot be created from PureScript other than by FFI, you should be declaring a foreign data type. The Browser type in Toppokki is defined in this way:

foreign import data Browser :: Type

:: Type

You might ask, "what is :: Type"? This is exactly like Type signatures of values you may have, like 1 :: Int or "string" :: String but for types, known as a Kind signature. These are "types of types", and by giving a type the kind signature Type, we are declaring that there exist values of this as Types are representable in the runtime.

For an example, see the documentation of Array, which has the signature

data Array :: Type -> Type

As a Type -> Type, this does not have a valid value. But by applying a Type argument, we can get a real type:

type ArrayInt = Array Int

aint = [1] :: ArrayInt

For further reading, see https://github.com/purescript/documentation/blob/master/language/Types.md#kind-system and https://leanpub.com/purescript/read#leanpub-auto-type-constructors-and-kinds. Once you have read those sections, you should look at the definition of SProxy and think about what it means: https://pursuit.purescript.org/packages/purescript-prelude/4.1.0/docs/Data.Symbol#t:SProxy

Uncurried Effect functions

This section is better explained in the docs for Effect.Uncurried here: https://pursuit.purescript.org/packages/purescript-effect/2.0.0/docs/Effect.Uncurried

As you might know, a typical PureScript function is actually a series of functions based on the number of arguments they take. For example, myFn :: Int -> String -> Boolean will give you a function function myFn (int) { return function (str) { return boolean }}, so this function is to be called myFn(1)("str") from JavaScript. A function of Effect is thunked, so an extra function surrounds the return value. For instance, function (i) { console.log(i) } would immediately cause an effect when the Int is supplied, so it is instead represented as function effFn (i) { return function () { console.log(i) }} and typed effFn :: Int -> Effect Unit.

Then, to define normal effectful functions with multiple arguments, we use functions from Effect.Uncurried and then use the runEffectFn_ functions to run these EffectFn_ functions, and mkEffectFn_ functions to create them. For example,

exports._on = function(event, callback, page) {
  return page.on(event, callback);
};

on_ takes three args here and causes an effect, so we foreign import this as

foreign import _on :: forall a. EU.EffectFn3 String (EU.EffectFn1 a Unit) Page Unit

With this, we can then make normal PureScript function that has curried arguments:

onLoad :: EU.EffectFn1 Unit Unit -> Page -> Effect Unit
onLoad = EU.runEffectFn3 _on "load"

It's very important to learn this to work with FFI with uncurried arguments, so it's worth visiting the links in this post to review this.

Thunked Promises to Aff

This section is explained more in the docs for PureScript-Aff-Promise here: https://pursuit.purescript.org/packages/purescript-aff-promise/2.0.0

Because merely creating a Promise in JavaScript will run them, we need to actually thunk even the creation of Promises in FFI. This means that all of our FFI functions need to return Effect (Promise Something).

Then we need to work with these Promises in our PureScript application. Almost all PureScript applications use the PureScript-Aff library to have reliable, fast asynchronous effects, so it's nicest to use the Aff type and convert to it. Luckily, we have just the function for it defined in Aff-Promise as toAffE:

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

So this lets us convert a thunked Promise into an Aff, so this lets us define FFI imports like this:

foreign import _newPage :: FU.Fn1 Browser (Effect (Promise Page))

and we can define a little helper function and define a normal function for newPage:

runPromiseAffE1 :: forall a o. FU.Fn1 a (Effect (Promise o)) -> a -> Aff o
runPromiseAffE1 f a = Promise.toAffE $ FU.runFn1 f a

newPage :: Browser -> Aff Page
newPage = runPromiseAffE1 _newPage

And there we have it, a normal function that uses Aff that we can call from PureScript without having to deal with its representation in the FFI.

Conclusion

Hopefully this has shown you some of the ways you can work with FFI and convert FFI imports into normal PureScript functions and values and empowered you to solve problems as you see fit, rather than to be entrapped by some restrictive, partial solutions.

Be sure to visit the links to see more in-depth explanations.

Extra

Readers also pointed out that I didn't talk about exposing foreign values as Foreign from the PureScript-Foreign library. This library along with Simple-JSON can be quite useful for verifying that some FFI result is what you expect in runtime.

Links

8
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
4