The Foreign Function Interface

Chapter Goals

This chapter will introduce PureScript's foreign function interface (or FFI), which enables communication from PureScript code to JavaScript code, and vice versa. We will cover how to:

  • Call pure, effectful, and asynchronous JavaScript functions from PureScript.
  • Work with untyped data.
  • Encode and parse JSON using the argonaut package.

Towards the end of this chapter, we will revisit our recurring address book example. The goal of the chapter will be to add the following new functionality to our application using the FFI:

  • Alert the user with a popup notification.
  • Store the serialized form data in the browser's local storage, and reload it when the application restarts.

There is also an addendum which covers some additional topics which are not as commonly sought-after. Feel free to read these sections, but don't let them stand in the way of progressing through the remainder of the book if they're less relevant to your learning objectives:

  • Understand the representation of PureScript values at runtime.
  • Call PureScript functions from JavaScript.

Project Setup

The source code for this module is a continuation of the source code from chapters 3, 7 and 8. As such, the source tree includes the appropriate source files from those chapters.

This chapter introduces the argonaut library as a dependency. This library is used for encoding and decoding JSON.

The exercises for this chapter should be written in test/Solutions.purs and can be checked against the unit tests in test/Main.purs by running spago test.

The Address Book app can be launched with parcel src/index.html --open. It uses the same workflow from Chapter 8, so refer to that chapter for more detailed instructions.

A Disclaimer

PureScript provides a straightforward foreign function interface to make working with JavaScript as simple as possible. However, it should be noted that the FFI is an advanced feature of the language. To use it safely and effectively, you should have an understanding of the runtime representation of the data you plan to work with. This chapter aims to impart such an understanding as pertains to code in PureScript's standard libraries.

PureScript's FFI is designed to be very flexible. In practice, this means that developers have a choice, between giving their foreign functions very simple types, or using the type system to protect against accidental misuses of foreign code. Code in the standard libraries tends to favor the latter approach.

As a simple example, a JavaScript function makes no guarantees that its return value will not be null. Indeed, idiomatic JavaScript code returns null quite frequently! However, PureScript's types are usually not inhabited by a null value. Therefore, it is the responsibility of the developer to handle these corner cases appropriately when designing their interfaces to JavaScript code using the FFI.

Calling JavaScript From PureScript

The simplest way to use JavaScript code from PureScript is to give a type to an existing JavaScript value using a foreign import declaration. Foreign import declarations should have a corresponding JavaScript declaration in a foreign JavaScript module.

For example, consider the encodeURIComponent function, which can be used from JavaScript to encode a component of a URI by escaping special characters:

$ node

node> encodeURIComponent('Hello World')
'Hello%20World'

This function has the correct runtime representation for the function type String -> String, since it takes non-null strings to non-null strings, and has no other side-effects.

We can assign this type to the function with the following foreign import declaration:

module Test.URI where

foreign import encodeURIComponent :: String -> String

We also need to write a foreign JavaScript module. If the module above is saved as test/URI.purs, then the foreign JavaScript module should be saved as test/URI.js:

"use strict";

exports.encodeURIComponent = encodeURIComponent;

Spago will find .js files in the src and test directories, and provide them to the compiler as foreign JavaScript modules.

JavaScript functions and values are exported from foreign JavaScript modules by assigning them to the exports object just like a regular CommonJS module. The purs compiler (wrapped by spago) treats this module like a regular CommonJS module, and simply adds it as a dependency to the compiled PureScript module. However, when bundling code for the browser with psc-bundle or spago bundle-app --to, it is very important to follow the pattern above, assigning exports to the exports object using a property assignment. This is because psc-bundle recognizes this format, allowing unused JavaScript exports to be removed from bundled code.

With these two pieces in place, we can now use the encodeURIComponent function from PureScript like any function written in PureScript. For example, if this declaration is saved as a module and loaded into PSCi, we can reproduce the calculation above:

$ spago repl

> import Data.URI
> encodeURIComponent "Hello World"
"Hello%20World"

We can also define our own functions in foreign modules. Here's an example of how to create and call a custom JavaScript function that squares a Number:

src/Test/Examples.js:

"use strict";

exports.square = function(n) {
  return n * n;
};

src/Test/Examples.purs:

module Test.Examples where

foreign import square :: Number -> Number
$ spago repl

> import Test.Examples
> square 5.0
25.0

Functions of Multiple Arguments

Let's rewrite our diagonal function from Chapter 2 in a foreign module to demonstrate how to call functions of multiple arguments. Recall that this function calculates the diagonal of a right-angled triangle:

exports.diagonal = function(w, h) {
  return Math.sqrt(w * w + h * h);
};

Because PureScript uses curried functions of single arguments, we cannot directly import the diagonal function of two arguments like so:

-- This will not work with above uncurried definition of diagonal
foreign import diagonal :: Number -> Number -> Number

However, there are a few solutions to this dilemma:

The first option is to import and run the function with an Fn wrapper from Data.Function.Uncurried (Fn and uncurried functions are discussed in more detail later):

foreign import diagonal :: Fn2 Number Number Number
$ spago repl

> import Test.Examples
> import Data.Function.Uncurried
> runFn2 diagonal 3.0 4.0
5.0

The second option is to wrap or rewrite the function as curried JavaScript:

exports.diagonalNested = function(w) {
  return function (h) {
    return Math.sqrt(w * w + h * h);
  };
};

or equivalently with arrow functions (see ES6 note below):

exports.diagonalArrow = w => h =>
  Math.sqrt(w * w + h * h);
foreign import diagonalNested :: Number -> Number -> Number
foreign import diagonalArrow  :: Number -> Number -> Number
$ spago repl

> import Test.Examples
> diagonalNested 3.0 4.0
5.0
> diagonalArrow 3.0 4.0
5.0

A Note About Uncurried Functions

PureScript's Prelude contains an interesting set of examples of foreign types. As we have covered already, PureScript's function types only take a single argument, and can be used to simulate functions of multiple arguments via currying. This has certain advantages - we can partially apply functions, and give type class instances for function types - but it comes with a performance penalty. For performance critical code, it is sometimes necessary to define genuine JavaScript functions which accept multiple arguments. The Prelude defines foreign types which allow us to work safely with such functions.

For example, the following foreign type declaration is taken from the Data.Function.Uncurried module:

foreign import data Fn2 :: Type -> Type -> Type -> Type

This defines the type constructor Fn2 which takes three type arguments. Fn2 a b c is a type representing JavaScript functions of two arguments of types a and b, and with return type c.

The functions package defines similar type constructors for function arities from 0 to 10.

We can create a function of two arguments by using the mkFn2 function, as follows:

import Data.Function.Uncurried

uncurriedAdd :: Fn2 Int Int Int
uncurriedAdd = mkFn2 \n m -> m + n

and we can apply a function of two arguments by using the runFn2 function:

$ spago repl

> import Test.Examples
> import Data.Function.Uncurried
> runFn2 uncurriedAdd 3 10
13

The key here is that the compiler inlines the mkFn2 and runFn2 functions whenever they are fully applied. The result is that the generated code is very compact:

var uncurriedAdd = function (n, m) {
  return m + n | 0;
};

For contrast, here is a traditional curried function:

curriedAdd :: Int -> Int -> Int
curriedAdd n m = m + n

and the resulting generated code, which is less compact due to the nested functions:

var curriedAdd = function (n) {
  return function (m) {
    return m + n | 0;
  };
};

A Note About Modern JavaScript Syntax

The arrow function syntax we saw earlier is an ES6 feature, and so it is incompatible with some older browsers (namely IE11). As of writing, it is estimated that arrow functions are unavailable for the 6% of users who have not yet updated their web browser.

In order to be compatible with the most users, the JavaScript code generated by the PureScript compiler does not use arrow functions. It is also recommended to avoid arrow functions in public libraries for the same reason.

You may still use arrow functions in your own FFI code, but then should include a tool such as Babel in your deployment workflow to convert these back to ES5 compatible functions.

If you find arrow functions in ES6 more readable, you may transform JavaScript code in the compiler's output directory with a tool like Lebab:

npm i -g lebab
lebab --replace output/ --transform arrow,arrow-return

This operation would convert the above curriedAdd function to:

var curriedAdd = n => m =>
  m + n | 0;

The remaining examples in this book will use arrow functions instead of nested functions.

Exercises

  1. (Medium) Write a JavaScript function volumeFn in the Test.Solutions module that finds the volume of a box. Use an Fn wrapper from Data.Function.Uncurried.
  2. (Medium) Rewrite volumeFn with arrow functions as volumeArrow.

Passing Simple Types

The following data types may be passed between PureScript and JavaScript as-is:

PureScriptJavaScript
BooleanBoolean
StringString
Int, NumberNumber
ArrayArray
RecordObject

We've already seen examples with the primitive types String and Number. We'll now take a look at the structural types Array and Record (Object in JavaScript).

To demonstrate passing Arrays, here's how to call a JavaScript function which takes an Array of Int and returns the cumulative sum as another array. Recall that, since JavaScript does not have a separate type for Int, both Int and Number in PureScript translate to Number in JavaScript.

foreign import cumulativeSums :: Array Int -> Array Int
exports.cumulativeSums = arr => {
  let sum = 0
  let sums = []
  arr.forEach(x => {
    sum += x;
    sums.push(sum);
  });
  return sums;
};
$ spago repl

> import Test.Examples
> cumulativeSums [1, 2, 3]
[1,3,6]

To demonstrate passing Records, here's how to call a JavaScript function which takes two Complex numbers as records, and returns their sum as another record. Note that a Record in PureScript is represented as an Object in JavaScript:

type Complex = {
  real :: Number,
  imag :: Number
}

foreign import addComplex :: Complex -> Complex -> Complex
exports.addComplex = a => b => {
  return {
    real: a.real + b.real,
    imag: a.imag + b.imag
  }
};
$ spago repl

> import Test.Examples
> addComplex { real: 1.0, imag: 2.0 } { real: 3.0, imag: 4.0 }
{ imag: 6.0, real: 4.0 }

Note that the above techniques require trusting that JavaScript will return the expected types, as PureScript is not able to apply type checking to JavaScript code. We will describe this type safety concern in more detail later on in the JSON section, as well as cover techniques to protect against type mismatches.

Exercises

  1. (Medium) Write a JavaScript function cumulativeSumsComplex (and corresponding PureScript foreign import) that takes an Array of Complex numbers and returns the cumulative sum as another array of complex numbers.

Beyond Simple Types

We have seen examples of how to send and receive types with a native JavaScript representation, such as String, Number, Array, and Record, over FFI. Now we'll cover how to use some of the other types available in PureScript, like Maybe.

Suppose we wanted to recreate the head function on arrays by using a foreign declaration. In JavaScript, we might write the function as follows:

exports.head = arr =>
  arr[0];

However, there is a problem with this function. We might try to give it the type forall a. Array a -> a, but for empty arrays, this function returns undefined. Therefore, this function does not have the correct runtime representation.

We can instead return a Maybe value to handle this corner case.

It is tempting to write the following:

// Don't do this
exports.maybeHead = arr => {
  if (arr.length) {
    return Data_Maybe.Just.create(arr[0]);
  } else {
    return Data_Maybe.Nothing.value;
  }
}
foreign import maybeHead :: forall a. Array a -> Maybe a

But calling these Maybe constructors directly in the FFI code isn't recommended as it makes the code brittle to changes in the code generator. Additionally, doing this can cause problems when using purs bundle for dead code elimination.

The recommended approach is to add extra parameters to your FFI-defined function to accept the functions you need to call as arguments:

exports.maybeHeadImpl = just => nothing => arr => {
  if (arr.length) {
    return just(arr[0]);
  } else {
    return nothing;
  }
};
foreign import maybeHeadImpl :: forall a. (forall x. x -> Maybe x) -> (forall x. Maybe x) -> Array a -> Maybe a

maybeHead :: forall a. Array a -> Maybe a
maybeHead arr = maybeHeadImpl Just Nothing arr

Note that we wrote:

forall a. (forall x. x -> Maybe x) -> (forall x. Maybe x) -> Array a -> Maybe a

and not:

forall a. ( a -> Maybe a) -> Maybe a -> Array a -> Maybe a

While both forms work, the latter is more vulnerable to unwanted inputs in place of Just and Nothing. For example, in the more vulnerable case we could call it as follows:

maybeHeadImpl (\_ -> Just 1000) (Just 1000) [1,2,3]

which returns Just 1000 for any array input. This vulnerability is allowed because (\_ -> Just 1000) and Just 1000 match the signatures of (a -> Maybe a) and Maybe a respectively when a is Int (based on input array).

In the more secure type signature, even when a is determined to be Int based on the input array, we still need to provide valid functions matching the signatures involving forall x. The only option for (forall x. Maybe x) is Nothing, since a Just would assume a type for x and then no longer be valid for all x. The only options for (forall x. x -> Maybe x) are Just (our desired argument) and (\_ -> Nothing), which is the only remaining vulnerability.

Defining Foreign Types

Suppose instead of returning a Maybe a, we wanted to return a new type Undefined a whose representation at runtime was like that for the type a, but also allowing the undefined value.

We can define a foreign type using the FFI using a foreign type declaration. The syntax is similar to defining a foreign function:

foreign import data Undefined :: Type -> Type

Note that the data keyword here indicates that we are defining a type, not a value. Instead of a type signature, we give the kind of the new type. In this case, we declare the kind of Undefined to be Type -> Type. In other words, Undefined is a type constructor.

We can now simply reuse our original definition for head:

exports.undefinedHead = arr =>
  arr[0];

And in the PureScript module:

foreign import undefinedHead :: forall a. Array a -> Undefined a

The body of the undefinedHead function returns arr[0] even if that value is undefined, and the type signature reflects the fact that our function can return an undefined value.

This function has the correct runtime representation for its type, but is quite useless since we have no way to use a value of type Undefined a. But we can fix that by writing some new functions using the FFI!

The most basic function we need will tell us whether a value is defined or not:

foreign import isUndefined :: forall a. Undefined a -> Boolean

This is easily defined in our foreign JavaScript module as follows:

exports.isUndefined = value =>
  value === undefined;

We can now use isUndefined and undefinedHead together from PureScript to define a useful function:

isEmpty :: forall a. Array a -> Boolean
isEmpty = isUndefined <<< undefinedHead

Here, the foreign function we defined is very simple, which means we can benefit from the use of PureScript's typechecker as much as possible. This is good practice in general: foreign functions should be kept as small as possible, and application logic moved into PureScript code wherever possible.

Exceptions

Another option is to simply throw an exception in the case of an empty array. Strictly speaking, pure functions should not throw exceptions, but we have the flexibility to do so. We indicate the lack of safety in the function name:

foreign import unsafeHead :: forall a. Array a -> a

In our foreign JavaScript module, we can define unsafeHead as follows:

exports.unsafeHead = arr => {
  if (arr.length) {
    return arr[0];
  } else {
    throw new Error('unsafeHead: empty array');
  }
};

Exercises

  1. (Medium) Given a record that represents a quadratic polynomial a*x^2 + b*x + c = 0:
    type Quadratic = {
      a :: Number,
      b :: Number,
      c :: Number
    }
    
    Write a JavaScript function quadraticRootsImpl and a wrapper quadraticRoots :: Quadratic -> Pair Complex that uses the quadratic formula to find the roots of this polynomial. Return the two roots as a Pair of Complex numbers. Hint: Use the quadraticRoots wrapper to pass a constructor for Pair to quadraticRootsImpl.

Using Type Class Member Functions

Just like our earlier guide on passing the Maybe constructor over FFI, this is another case of writing PureScript that calls JavaScript, which in turn calls PureScript functions again. Here we will explore how to pass type class member functions over the FFI.

We start with writing a foreign JavaScript function which expects the appropriate instance of show to match the type of x.

exports.boldImpl = show => x =>
  show(x).toUpperCase() + "!!!";

Then we write the matching signature:

foreign import boldImpl :: forall a. (a -> String) -> a -> String

and a wrapper function that passes the correct instance of show:

bold :: forall a. Show a => a -> String
bold x = boldImpl show x

Alternatively in point-free form:

bold :: forall a. Show a => a -> String
bold = boldImpl show

We can then call the wrapper:

$ spago repl

> import Test.Examples
> import Data.Tuple
> bold (Tuple 1 "Hat")
"(TUPLE 1 \"HAT\")!!!"

Here's another example demonstrating passing multiple functions, including a function of multiple arguments (eq):

exports.showEqualityImpl = eq => show => a => b => {
  if (eq(a)(b)) {
    return "Equivalent";
  } else {
    return show(a) + " is not equal to " + show(b);
  }
}
foreign import showEqualityImpl :: forall a. (a -> a -> Boolean) -> (a -> String) -> a -> a -> String

showEquality :: forall a. Eq a => Show a => a -> a -> String
showEquality = showEqualityImpl eq show
$ spago repl

> import Test.Examples
> import Data.Maybe
> showEquality Nothing (Just 5)
"Nothing is not equal to (Just 5)"

Effectful Functions

Let's extend our bold function to log to the console. Logging is an Effect, and Effects are represented in JavaScript as a function of zero arguments, () with arrow notation:

exports.yellImpl = show => x => () =>
  console.log(show(x).toUpperCase() + "!!!");

The new foreign import is the same as before, except that the return type changed from String to Effect Unit.

foreign import yellImpl :: forall a. (a -> String) -> a -> Effect Unit

yell :: forall a. Show a => a -> Effect Unit
yell = yellImpl show

When testing this in the repl, notice that the string is printed directly to the console (instead of being quoted) and a unit value is returned.

$ spago repl

> import Test.Examples
> import Data.Tuple
> yell (Tuple 1 "Hat")
(TUPLE 1 "HAT")!!!
unit

There are also EffectFn wrappers from Effect.Uncurried. These are similar to the Fn wrappers from Data.Function.Uncurried that we've already seen. These wrappers let you call uncurried effectful functions in PureScript.

You'd generally only use these if you want to call existing JavaScript library APIs directly, rather than wrapping those APIs in curried functions. So it doesn't make much sense to present an example of uncurried yell, where the JavaScript relies on PureScript type class members, since you wouldn't find that in the existing JavaScript ecosystem.

Instead, we'll modify our previous diagonal example to include logging in addition to returning the result:

exports.diagonalLog = function(w, h) {
  let result = Math.sqrt(w * w + h * h);
  console.log("Diagonal is " + result);
  return result;
};
foreign import diagonalLog :: EffectFn2 Number Number Number
$ spago repl

> import Test.Examples
> import Effect.Uncurried
> runEffectFn2 diagonalLog 3.0 4.0
Diagonal is 5
5.0

Asynchronous Functions

Promises in JavaScript translate directly to asynchronous effects in PureScript with the help of the aff-promise library. See that library's documentation for more information. We'll just go through a few examples.

Suppose we want to use this JavaScript wait promise (or asynchronous function) in our PureScript project. It may be used to delay execution for ms milliseconds.

const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

We just need to export it wrapped as an Effect (function of zero arguments):

exports.sleepImpl = ms => () =>
  wait(ms);

Then import it as follows:

foreign import sleepImpl :: Int -> Effect (Promise Unit)

sleep :: Int -> Aff Unit
sleep = sleepImpl >>> toAffE

We can then run this Promise in an Aff block like so:

$ spago repl

> import Prelude
> import Test.Examples
> import Effect.Class.Console
> import Effect.Aff
> :pa
… launchAff_ do
…   log "waiting"
…   sleep 300
…   log "done waiting"
…
waiting
unit
done waiting

Note that asynchronous logging in the repl just waits to print until the entire block has finished executing. This code behaves more predictably when run with spago test where there is a slight delay between prints.

Let's look at another example where we return a value from a promise. This function is written with async and await, which is just syntactic sugar for promises.

async function diagonalWait(delay, w, h) {
  await wait(delay);
  return Math.sqrt(w * w + h * h);
}

exports.diagonalAsyncImpl = delay => w => h => () =>
  diagonalWait(delay, w, h);

Since we're returning a Number, we represent this type in the Promise and Aff wrappers:

foreign import diagonalAsyncImpl :: Int -> Number -> Number -> Effect (Promise Number)

diagonalAsync :: Int -> Number -> Number -> Aff Number
diagonalAsync i x y = toAffE $ diagonalAsyncImpl i x y
$ spago repl

import Prelude
import Test.Examples
import Effect.Class.Console
import Effect.Aff
> :pa
… launchAff_ do
…   res <- diagonalAsync 300 3.0 4.0
…   logShow res
…
unit
5.0

Exercises

Exercises for the above sections are still on the ToDo list. If you have any ideas for good exercises, please make a suggestion.

JSON

There are many reasons to use JSON in an application, for example, it's a common means of communicating with web APIs. This section will discuss other use-cases too, beginning with a technique to improve type safety when passing structural data over the FFI.

Let's revisit our earlier FFI functions cumulativeSums and addComplex and introduce a bug to each:

exports.cumulativeSumsBroken = arr => {
  let sum = 0
  let sums = []
  arr.forEach(x => {
    sum += x;
    sums.push(sum);
  });
  sums.push("Broken"); // Bug
  return sums;
};

exports.addComplexBroken = a => b => {
  return {
    real: a.real + b.real,
    broken: a.imag + b.imag // Bug
  }
};

We can use the original type signatures, and the code will still compile, despite the fact that the return types are incorrect.

foreign import cumulativeSumsBroken :: Array Int -> Array Int

foreign import addComplexBroken :: Complex -> Complex -> Complex

We can even execute the code, which might either produce unexpected results or a runtime error:

$ spago repl

> import Test.Examples
> import Data.Foldable (sum)

> sums = cumulativeSumsBroken [1, 2, 3]
> sums
[1,3,6,Broken]
> sum sums
0

> complex = addComplexBroken { real: 1.0, imag: 2.0 } { real: 3.0, imag: 4.0 }
> complex.real
4.0
> complex.imag + 1.0
NaN
> complex.imag
  var str = n.toString();
              ^
TypeError: Cannot read property 'toString' of undefined

For example, our resulting sums is no-longer a valid Array Int, now that a String is included in the Array. And further operations produce unexpected behavior, rather than an outright error, as the sum of these sums is 0 rather than 10. This could be a difficult bug to track down!

Likewise, there are no errors when calling addComplexBroken; however, accessing the imag field of our Complex result will either produce unexpected behavior (returning NaN instead of 7.0), or a non-obvious runtime error.

Let's use JSON to make our PureScript code more impervious to bugs in JavaScript code.

The argonaut library contains the JSON decoding and encoding capabilities we need. That library has excellent documentation, so we will only cover basic usage in this book.

If we create an alternate foreign import that defines the return type as Json:

foreign import cumulativeSumsJson :: Array Int -> Json
foreign import addComplexJson :: Complex -> Complex -> Json

Note that we're simply pointing to our existing broken functions:

exports.cumulativeSumsJson = exports.cumulativeSumsBroken
exports.addComplexJson = exports.addComplexBroken

And then write a wrapper to decode the returned foreign Json value:

cumulativeSumsDecoded :: Array Int ->  Either String (Array Int)
cumulativeSumsDecoded arr = decodeJson $ cumulativeSumsJson arr

addComplexDecoded :: Complex -> Complex ->  Either String Complex
addComplexDecoded a b = decodeJson $ addComplexJson a b

Then any values that can't be successfully decoded to our return type appear as a Left error String:

$ spago repl

> import Test.Examples

> cumulativeSumsDecoded [1, 2, 3]
(Left "Couldn't decode Array (Failed at index 3): Value is not a Number")

> addComplexDecoded { real: 1.0, imag: 2.0 } { real: 3.0, imag: 4.0 }
(Left "JSON was missing expected field: imag")

If we call the working versions, a Right value is returned.

Try this yourself by modifying test/Examples.js with the following change to point to the working versions before running the next repl block.

exports.cumulativeSumsJson = exports.cumulativeSums
exports.addComplexJson = exports.addComplex
$ spago repl

> import Test.Examples

> cumulativeSumsDecoded [1, 2, 3]
(Right [1,3,6])

> addComplexDecoded { real: 1.0, imag: 2.0 } { real: 3.0, imag: 4.0 }
(Right { imag: 6.0, real: 4.0 })

Using JSON is also the easiest way to pass other structural types, such as Map and Set through the FFI. Note that since JSON only consists of booleans, numbers, strings, arrays, and objects of other JSON values, we can't write a Map and Set directly in JSON. But we can represent these structures as arrays (assuming the keys and values can also be represented in JSON), and then decode them back to Map or Set.

Here's an example of a foreign function signature that modifies a Map of String keys and Int values, along with the wrapper function that handles JSON encoding and decoding.

foreign import mapSetFooJson :: Json -> Json

mapSetFoo :: Map String Int -> Either String (Map String Int)
mapSetFoo m = decodeJson $ mapSetFooJson $ encodeJson m

Note that this is a prime use case for function composition. Both of these alternatives are equivalent to the above:

mapSetFoo :: Map String Int -> Either String (Map String Int)
mapSetFoo = decodeJson <<< mapSetFooJson <<< encodeJson

mapSetFoo :: Map String Int -> Either String (Map String Int)
mapSetFoo = encodeJson >>> mapSetFooJson >>> decodeJson

Here is the JavaScript implementation. Note the Array.from step which is necessary to convert the JavaScript Map into a JSON-friendly format before decoding converts it back to a PureScript Map.

exports.mapSetFooJson = j => {
  let m = new Map(j);
  m.set("Foo", 42);
  return Array.from(m);
};

Now we can send and receive a Map over the FFI:

$ spago repl

> import Test.Examples
> import Data.Map
> import Data.Tuple

> myMap = fromFoldable [ Tuple "hat" 1, Tuple "cat" 2 ]

> :type myMap
Map String Int

> myMap
(fromFoldable [(Tuple "cat" 2),(Tuple "hat" 1)])

> mapSetFoo myMap
(Right (fromFoldable [(Tuple "Foo" 42),(Tuple "cat" 2),(Tuple "hat" 1)]))

Exercises

  1. (Medium) Write a JavaScript function and PureScript wrapper valuesOfMap :: Map String Int -> Either String (Set Int) that returns a Set of all the values in a Map. Hint: The .values() instance method for Map may be useful in your JavaScript code.

  2. (Easy) Write a new wrapper for the previous JavaScript function with the signature valuesOfMapGeneric :: forall k v. Map k v -> Either String (Set v) so it works with a wider variety of maps. Note that you'll need to add some type class constraints for k and v. The compiler will guide you.

  3. (Medium) Rewrite the earlier quadraticRoots function as quadraticRootsSet which returns the Complex roots as a Set via JSON (instead of as a Pair).

  4. (Difficult) Rewrite the earlier quadraticRoots function as quadraticRootsSafe which uses JSON to pass the Pair of Complex roots over FFI. Don't use the Pair constructor in JavaScript, but instead, just return the pair in a decoder-compatible format. Hint: You'll need to write a DecodeJson instance for Pair. Consult the argonaut docs for instruction on writing your own decode instance. Their decodeJsonTuple instance may also be a helpful reference. Note that you'll need a newtype wrapper for Pair to avoid creating an "orphan instance".

  5. (Medium) Write a decodeArray2D :: String -> Either String (Array (Array Int)) function to parse and decode a JSON string containing a 2D array, such as "[[1, 2, 3], [4, 5], [6]]". Hint: You'll need to use jsonParser to convert the String into Json before decoding.

  6. (Medium) The following data type represents a binary tree with values at the leaves:

    data Tree a
      = Leaf a
      | Branch (Tree a) (Tree a)
    

    Derive generic EncodeJson and DecodeJson instances for the Tree type. Consult the argonaut docs for instructions on how to do this. Note that you'll also need generic instances of Show and Eq to enable unit testing for this exercise, but those should be straightforward to implement after tackling the JSON instances.

  7. (Difficult) The following data type should be represented directly in JSON as either an integer or a string:

    data IntOrString
      = IntOrString_Int Int
      | IntOrString_String String
    

    Write instances of EncodeJson and DecodeJson for the IntOrString data type which implement this behavior. Hint: The alt operator from Control.Alt may be helpful.

Address book

In this section we will apply our newly-acquired FFI and JSON knowledge to build on our address book example from chapter 8. We will add the following features:

  • A Save button at the bottom of the form that, when clicked, serializes the state of the form to JSON and saves it in local storage.
  • Automatic retrieval of the JSON document from local storage upon page reload. The form fields are populated with the contents of this document.
  • A pop-up alert if there is an issue saving or loading the form state.

We'll start by creating FFI wrappers for the following Web Storage APIs in our Effect.Storage module:

  • setItem takes a key and a value (both strings), and returns a computation which stores (or updates) the value in local storage at the specified key.
  • getItem takes a key, and attempts to retrieve the associated value from local storage. However, since the getItem method on window.localStorage can return null, the return type is not String, but Json.
foreign import setItem :: String -> String -> Effect Unit

foreign import getItem :: String -> Effect Json

Here is the corresponding JavaScript implementation of these functions in Effect/Storage.js:

exports.setItem = key => value => () =>
  window.localStorage.setItem(key, value);

exports.getItem = key => () =>
  window.localStorage.getItem(key);

We'll create a save button like so:

saveButton :: R.JSX
saveButton =
  D.label
    { className: "form-group row col-form-label"
    , children:
        [ D.button
            { className: "btn-primary btn"
            , onClick: handler_ validateAndSave
            , children: [ D.text "Save" ]
            }
        ]
    }

And write our validated person as a JSON string with setItem in the validateAndSave function:

validateAndSave :: Effect Unit
validateAndSave = do
  log "Running validators"
  case validatePerson' person of
    Left errs -> log $ "There are " <> show (length errs) <> " validation errors."
    Right validPerson -> do
      setItem "person" $ stringify $ encodeJson validPerson
      log "Saved"

Note that if we attempt to compile at this stage, we'll encounter the following error:

  No type class instance was found for
    Data.Argonaut.Encode.Class.EncodeJson PhoneType

This is because PhoneType in the Person record needs an EncodeJson instance. We'll just derive a generic encode instance, and an encode instance too while we're at it. More information how this works is available in the argonaut docs:

import Data.Argonaut (class DecodeJson, class EncodeJson)
import Data.Argonaut.Encode.Generic.Rep (genericEncodeJson)
import Data.Argonaut.Decode.Generic.Rep (genericDecodeJson)
import Data.Generic.Rep (class Generic)

derive instance genericPhoneType :: Generic PhoneType _

instance encodeJsonPhoneType :: EncodeJson PhoneType where
  encodeJson = genericEncodeJson

instance decodeJsonPhoneType :: DecodeJson PhoneType where
  decodeJson = genericDecodeJson

Now we can save our person to local storage, but this isn't very useful unless we can retrieve the data. We'll tackle that next.

We'll start with retrieving the "person" string from local storage:

item <- getItem "person"

Then we'll create a helper function to handle converting the string from local storage to our Person record. Note that this string in storage may be null, so we represent it as a foreign Json until it is successfully decoded as a String. There are a number of other conversion steps along the way - each of which return an Either value, so it makes sense to organize these together in a do block.

processItem :: Json -> Either String Person
processItem item = do
  jsonString <- decodeJson item
  j <- jsonParser jsonString
  (p :: Person) <- decodeJson j
  pure p

Then we inspect this result to see if it succeeded. If it failed, we'll log the errors and use our default examplePerson, otherwise we'll use the person retrieved from local storage.

initialPerson <- case processItem item of
  Left err -> do
    log $ "Error: " <> err <> ". Loading examplePerson"
    pure examplePerson
  Right p -> pure p

Finally, we'll pass this initialPerson to our component via the props record:

-- Create JSX node from react component.
app = element addressBookApp { initialPerson }

And pick it up on the other side to use in our state hook:

mkAddressBookApp :: Effect (ReactComponent { initialPerson :: Person })
mkAddressBookApp =
  reactComponent "AddressBookApp" \props -> R.do
    Tuple person setPerson <- useState props.initialPerson

As a finishing touch, we'll improve the quality of our error messages by appending to the String of each Left value with lmap.

processItem :: Json -> Either String Person
processItem item = do
  jsonString <- lmap ("No string in local storage: " <> _) $ decodeJson item
  j <- lmap ("Cannot parse JSON string: " <> _) $ jsonParser jsonString
  (p :: Person) <- lmap ("Cannot decode Person: " <> _) $ decodeJson j
  pure p

Only the first error should ever occur during normal operation of this app. You can trigger the other errors by opening your web browser's dev tools, editing the saved "person" string in local storage, and refreshing the page. How you modify the JSON string determines which error is triggered. See if you can trigger each of them.

That covers local storage. Next we'll implement the alert action, which is very similar to the log action from the Effect.Console module. The only difference is that the alert action uses the window.alert method, whereas the log action uses the console.log method. As such, alert can only be used in environments where window.alert is defined, such as a web browser.

foreign import alert :: String -> Effect Unit
exports.alert = msg => () =>
  window.alert(msg);

We want this alert to appear when either:

  • A user attempts to save a form with validation errors.
  • The state cannot be retrieved from local storage.

That is accomplished by simply replacing log with alert on these lines:

Left errs -> alert $ "There are " <> show (length errs) <> " validation errors."

alert $ "Error: " <> err <> ". Loading examplePerson"

Exercises

  1. (Easy) Write a wrapper for the removeItem method on the localStorage object, and add your foreign function to the Effect.Storage module.
  2. (Medium) Add a "Reset" button that, when clicked, calls the newly-created removeItem function to delete the "person" entry from local storage.
  3. (Easy) Write a wrapper for the confirm method on the JavaScript Window object, and add your foreign function to the Effect.Alert module.
  4. (Medium) Call this confirm function when a users clicks the "Reset" button to ask if they're sure they want to reset their address book.

Conclusion

In this chapter, we've learned how to work with foreign JavaScript code from PureScript and we've seen the issues involved with writing trustworthy code using the FFI:

  • We've seen the importance of ensuring that foreign functions have correct representations.
  • We learned how to deal with corner cases like null values and other types of JavaScript data, by using foreign types, or the Json data type.
  • We saw how to safely serialize and deserialize JSON data.

For more examples, the purescript, purescript-contrib and purescript-node GitHub organizations provide plenty of examples of libraries which use the FFI. In the remaining chapters, we will see some of these libraries put to use to solve real-world problems in a type-safe way.

Addendum

Calling PureScript from JavaScript

Calling a PureScript function from JavaScript is very simple, at least for functions with simple types.

Let's take the following simple module as an example:

module Test where

gcd :: Int -> Int -> Int
gcd 0 m = m
gcd n 0 = n
gcd n m
  | n > m     = gcd (n - m) m
  | otherwise = gcd (m - n) n

This function finds the greatest common divisor of two numbers by repeated subtraction. It is a nice example of a case where you might like to use PureScript to define the function, but have a requirement to call it from JavaScript: it is simple to define this function in PureScript using pattern matching and recursion, and the implementor can benefit from the use of the type checker.

To understand how this function can be called from JavaScript, it is important to realize that PureScript functions always get turned into JavaScript functions of a single argument, so we need to apply its arguments one-by-one:

var Test = require('Test');
Test.gcd(15)(20);

Here, I am assuming that the code was compiled with spago build, which compiles PureScript modules to CommonJS modules. For that reason, I was able to reference the gcd function on the Test object, after importing the Test module using require.

You might also like to bundle JavaScript code for the browser, using spago bundle-app --to file.js. In that case, you would access the Test module from the global PureScript namespace, which defaults to PS:

var Test = PS.Test;
Test.gcd(15)(20);

Understanding Name Generation

PureScript aims to preserve names during code generation as much as possible. In particular, most identifiers which are neither PureScript nor JavaScript keywords can be expected to be preserved, at least for names of top-level declarations.

If you decide to use a JavaScript keyword as an identifier, the name will be escaped with a double dollar symbol. For example,

null = []

generates the following JavaScript:

var $$null = [];

In addition, if you would like to use special characters in your identifier names, they will be escaped using a single dollar symbol. For example,

example' = 100

generates the following JavaScript:

var example$prime = 100;

Where compiled PureScript code is intended to be called from JavaScript, it is recommended that identifiers only use alphanumeric characters, and avoid JavaScript keywords. If user-defined operators are provided for use in PureScript code, it is good practice to provide an alternative function with an alphanumeric name for use in JavaScript.

Runtime Data Representation

Types allow us to reason at compile-time that our programs are "correct" in some sense - that is, they will not break at runtime. But what does that mean? In PureScript, it means that the type of an expression should be compatible with its representation at runtime.

For that reason, it is important to understand the representation of data at runtime to be able to use PureScript and JavaScript code together effectively. This means that for any given PureScript expression, we should be able to understand the behavior of the value it will evaluate to at runtime.

The good news is that PureScript expressions have particularly simple representations at runtime. It should always be possible to understand the runtime data representation of an expression by considering its type.

For simple types, the correspondence is almost trivial. For example, if an expression has the type Boolean, then its value v at runtime should satisfy typeof v === 'boolean'. That is, expressions of type Boolean evaluate to one of the (JavaScript) values true or false. In particular, there is no PureScript expression of type Boolean which evaluates to null or undefined.

A similar law holds for expressions of type Int Number and String - expressions of type Int or Number evaluate to non-null JavaScript numbers, and expressions of type String evaluate to non-null JavaScript strings. Expressions of type Int will evaluate to integers at runtime, even though they cannot not be distinguished from values of type Number by using typeof.

What about some more complex types?

As we have already seen, PureScript functions correspond to JavaScript functions of a single argument. More precisely, if an expression f has type a -> b for some types a and b, and an expression x evaluates to a value with the correct runtime representation for type a, then f evaluates to a JavaScript function, which when applied to the result of evaluating x, has the correct runtime representation for type b. As a simple example, an expression of type String -> String evaluates to a function which takes non-null JavaScript strings to non-null JavaScript strings.

As you might expect, PureScript's arrays correspond to JavaScript arrays. But remember - PureScript arrays are homogeneous, so every element has the same type. Concretely, if a PureScript expression e has type Array a for some type a, then e evaluates to a (non-null) JavaScript array, all of whose elements have the correct runtime representation for type a.

We've already seen that PureScript's records evaluate to JavaScript objects. Just as for functions and arrays, we can reason about the runtime representation of data in a record's fields by considering the types associated with its labels. Of course, the fields of a record are not required to be of the same type.

Representing ADTs

For every constructor of an algebraic data type, the PureScript compiler creates a new JavaScript object type by defining a function. Its constructors correspond to functions which create new JavaScript objects based on those prototypes.

For example, consider the following simple ADT:

data ZeroOrOne a = Zero | One a

The PureScript compiler generates the following code:

function One(value0) {
    this.value0 = value0;
};

One.create = function (value0) {
    return new One(value0);
};

function Zero() {
};

Zero.value = new Zero();

Here, we see two JavaScript object types: Zero and One. It is possible to create values of each type by using JavaScript's new keyword. For constructors with arguments, the compiler stores the associated data in fields called value0, value1, etc.

The PureScript compiler also generates helper functions. For constructors with no arguments, the compiler generates a value property, which can be reused instead of using the new operator repeatedly. For constructors with one or more arguments, the compiler generates a create function, which takes arguments with the appropriate representation and applies the appropriate constructor.

What about constructors with more than one argument? In that case, the PureScript compiler also creates a new object type, and a helper function. This time, however, the helper function is curried function of two arguments. For example, this algebraic data type:

data Two a b = Two a b

generates this JavaScript code:

function Two(value0, value1) {
    this.value0 = value0;
    this.value1 = value1;
};

Two.create = function (value0) {
    return function (value1) {
        return new Two(value0, value1);
    };
};

Here, values of the object type Two can be created using the new keyword, or by using the Two.create function.

The case of newtypes is slightly different. Recall that a newtype is like an algebraic data type, restricted to having a single constructor taking a single argument. In this case, the runtime representation of the newtype is actually the same as the type of its argument.

For example, this newtype representing telephone numbers:

newtype PhoneNumber = PhoneNumber String

is actually represented as a JavaScript string at runtime. This is useful for designing libraries, since newtypes provide an additional layer of type safety, but without the runtime overhead of another function call.

Representing Quantified Types

Expressions with quantified (polymorphic) types have restrictive representations at runtime. In practice, this means that there are relatively few expressions with a given quantified type, but that we can reason about them quite effectively.

Consider this polymorphic type, for example:

forall a. a -> a

What sort of functions have this type? Well, there is certainly one function with this type - namely, the identity function, defined in the Prelude:

id :: forall a. a -> a
id a = a

In fact, the identity function is the only (total) function with this type! This certainly seems to be the case (try writing an expression with this type which is not observably equivalent to identity), but how can we be sure? We can be sure by considering the runtime representation of the type.

What is the runtime representation of a quantified type forall a. t? Well, any expression with the runtime representation for this type must have the correct runtime representation for the type t for any choice of type a. In our example above, a function of type forall a. a -> a must have the correct runtime representation for the types String -> String, Number -> Number, Array Boolean -> Array Boolean, and so on. It must take strings to strings, numbers to numbers, etc.

But that is not enough - the runtime representation of a quantified type is more strict than this. We require any expression to be parametrically polymorphic - that is, it cannot use any information about the type of its argument in its implementation. This additional condition prevents problematic implementations such as the following JavaScript function from inhabiting a polymorphic type:

function invalid(a) {
    if (typeof a === 'string') {
        return "Argument was a string.";
    } else {
        return a;
    }
}

Certainly, this function takes strings to strings, numbers to numbers, etc. but it does not meet the additional condition, since it inspects the (runtime) type of its argument, so this function would not be a valid inhabitant of the type forall a. a -> a.

Without being able to inspect the runtime type of our function argument, our only option is to return the argument unchanged, and so identity is indeed the only inhabitant of the type forall a. a -> a.

A full discussion of parametric polymorphism and parametricity is beyond the scope of this book. Note however, that since PureScript's types are erased at runtime, a polymorphic function in PureScript cannot inspect the runtime representation of its arguments (without using the FFI), and so this representation of polymorphic data is appropriate.

Representing Constrained Types

Functions with a type class constraint have an interesting representation at runtime. Because the behavior of the function might depend on the type class instance chosen by the compiler, the function is given an additional argument, called a type class dictionary, which contains the implementation of the type class functions provided by the chosen instance.

For example, here is a simple PureScript function with a constrained type which uses the Show type class:

shout :: forall a. Show a => a -> String
shout a = show a <> "!!!"

The generated JavaScript looks like this:

var shout = function (dict) {
    return function (a) {
        return show(dict)(a) + "!!!";
    };
};

Notice that shout is compiled to a (curried) function of two arguments, not one. The first argument dict is the type class dictionary for the Show constraint. dict contains the implementation of the show function for the type a.

We can call this function from JavaScript by passing an explicit type class dictionary from Data.Show as the first parameter:

shout(require('Data.Show').showNumber)(42);

Exercises

  1. (Easy) What are the runtime representations of these types?

    forall a. a
    forall a. a -> a -> a
    forall a. Ord a => Array a -> Boolean
    

    What can you say about the expressions which have these types?

  2. (Medium) Try using the functions defined in the arrays package, calling them from JavaScript, by compiling the library using spago build and importing modules using the require function in NodeJS. Hint: you may need to configure the output path so that the generated CommonJS modules are available on the NodeJS module path.

Representing Side Effects

The Effect monad is also defined as a foreign type. Its runtime representation is quite simple - an expression of type Effect a should evaluate to a JavaScript function of no arguments, which performs any side-effects and returns a value with the correct runtime representation for type a.

The definition of the Effect type constructor is given in the Effect module as follows:

foreign import data Effect :: Type -> Type

As a simple example, consider the random function defined in the random package. Recall that its type was:

foreign import random :: Effect Number

The definition of the random function is given here:

exports.random = Math.random;

Notice that the random function is represented at runtime as a function of no arguments. It performs the side effect of generating a random number, and returns it, and the return value matches the runtime representation of the Number type: it is a non-null JavaScript number.

As a slightly more interesting example, consider the log function defined by the Effect.Console module in the console package. The log function has the following type:

foreign import log :: String -> Effect Unit

And here is its definition:

exports.log = function (s) {
  return function () {
    console.log(s);
  };
};

The representation of log at runtime is a JavaScript function of a single argument, returning a function of no arguments. The inner function performs the side-effect of writing a message to the console.

Expressions of type Effect a can be invoked from JavaScript like regular JavaScript methods. For example, since the main function is required to have type Effect a for some type a, it can be invoked as follows:

require('Main').main();

When using spago bundle-app --to or spago run, this call to main is generated automatically, whenever the Main module is defined.