polar

JavaScript Interop

Polar compiles to ES modules, so working with JavaScript goes both ways. Polar can call JavaScript functions, implement effects in JavaScript, and be imported by JavaScript and TypeScript code.

Calling JavaScript with externs

An extern gives a JavaScript function a Polar type. Say we have src/text.js:

export function slugify(text) {
  return text.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-");
}

export function first_word(text) {
  const match = text.match(/\w+/);
  return match ? match[0] : null;
}

export function words(text) {
  return text.split(/\s+/).filter(Boolean);
}

Declare each function in the externs zone, with its type, the file, and the name of the export:

externs
  slugify(text: String) -> String = "./text.js" slugify
  first_word(text: String) -> Option<String> = "./text.js" first_word
  words(text: String) -> List<String> = "./text.js" words

The path is relative to the .px file, and polar build copies the JavaScript file into dist/. After that, externs are called like any Polar function: slugify("Hello, World").

Note: Polar trusts the type you write. If slugify actually returned a number, Polar couldn't tell. Keep externs small, and test them.

Externs are synchronous

An extern called from an ordinary function is a plain, synchronous call. JavaScript that returns a Promise, like a network request or a timer, belongs behind an effect, which can be asynchronous. That's the next section.

Implementing an effect in JavaScript

A bind can point an entire effect at a JavaScript module. Each operation becomes an export with the same name:

effects
  Clock in Node {
    now() -> Int
    wait(ms: Int) -> {}
  }

binds
  Clock in Node = "./clock.js"
// src/clock.js
export function now() {
  return Date.now();
}

export async function wait(ms) {
  await new Promise((resolve) => setTimeout(resolve, ms));
}

Operations can be async. Polar awaits them for you, and Polar code calling Clock.wait(50) reads as if it were synchronous. The standard library's Dom and Process effects are written exactly this way.

Tip: Polar only makes functions async when they use an effect that could wait. Pure functions compile to plain synchronous JavaScript, with no await overhead.

How values convert

At the boundary, Polar converts values based on the declared types:

Polar type Polar to JavaScript JavaScript to Polar
Int, Float, String, Bool Unchanged Unchanged
{} Unchanged {}, whatever was returned
Option<T> None becomes null, Some(x) becomes x null and undefined become None, anything else Some
List<T> An array Any iterable
Result<E, A> Ok(x) becomes { ok: x }, Err(e) becomes { err: e } An object with an ok or err key. Anything else is a runtime error naming the operation
Records Objects, with fields converted by these rules The same, in reverse
Variants Their tagged object form Their tagged object form

Conversions nest, so an Option<List<Int>>, a Result<String, List<Int>> or a record field holding a list convert too. With the externs above:

Option.with_default(first_word("...  "), "(no words)")   // "(no words)", JS returned null
List.length(words("a b  c"))                             // 3, JS returned an array

Using Polar from JavaScript

Every exported Polar function is a named ES module export:

import { cents, add, format } from "./dist/money.js";

console.log(format(add(cents(1050), cents(99))));   // $11.49

Functions that use effects are async in JavaScript, so await them.

TypeScript

Each module also gets a .d.ts file, so TypeScript knows the types of everything you export:

export type Money = { cents: number };

export declare function cents(n: number): Money;

export declare function format(m: Money): string;

Effectful functions are declared as returning a Promise. For example, the browser start function from Running in the Browser is declared as start(): Promise<Record<string, never>>.