Strongly Typed JavaScript and Monads

I’ve been thinking about how to make working with b8r [even] easier. I’d like to make function signatures cleaner and more consistent and leverage autocomplete (right now I’m using vscode, but ideally do this in a way that’s highly adaptable).

Two-and-a-half approaches have suggested themselves:

  • Rewrite b8r in TypeScript. I actually started and made some progress on a complete port. But, it got pretty painful, especially when it might have been useful.
  • Create .d.ts files, preferably mechanically, that achieve the ease-of-use benefits of TypeScript output without the annoyance of actually writing TypeScript.
  • Extend b8r‘s work-in-progress type system in some way that helps this, for example by emitting .d.ts files.

There’s a lot of low-level code in b8r that is difficult or impossible to declare in TypeScript for without structural changes that can introduce subtle bugs (experience has shown me that writing nice TypeScript gets pretty gnarly for quite simple things) and I didn’t want to cheat. What would be the point?

If you write plain javascript functions in a specific way, you can leverage vscode’sTypeScript-powered autocomplete without the bother of actually writing TypeScript.

A common problem with code libraries (including b8r) is inconsistent parameter ordering, usually caused by unanticipated use-cases, which is exacerbated by parameters with default values having to trail the parameter list. Because b8r went into production use before it was mature, a lot of new paths were paved, sometimes favoring ease-of-use over consistency. E.g. there are several cases where, in pursuit of convenience, some commonly used b8r functions ended up with overloaded parameter handling, which would make even “proper” type declarations pretty unhelpful.

// is it element, eventType or eventType, element?
b8r.trigger( ... )

// so are r and s {top, left, width, height} or {x, y, width, height}
if (b8r.rectsOverlap(r, s)) { ...

// what does this function's signature look like?
b8r.setByPath('foo', 'bar.baz', 17)
b8r.setByPath('foo.bar.baz', 17)

Another thing I have long admired is the way Objective-C’s verbose APIs make toolbox calls both easy to read and easy to write. It might take a little more typing, but there’s no doubt what’s going on:

// pretty hard to screw this up
b8r.trigger({ element: button, eventType: 'click' })

// easy to read, maybe not so easy to write
if (b8r.rectsOverlap({ left: 100, top: 100, ... 

// it's clear what's going on in both cases
b8r.setByPath({ model: 'foo', path: 'bar.baz', newValue: 17 })
b8r.setByPath({ path: 'foo.bar.baz', newValue: 17 })

If I were just going to cheat, I could automatically generate .d.ts files from live objects (i.e. introspect the b8r object and generate type definitions directly from it, possibly tweaking them by hand). This could leverage the functionality in byExample, which is used by b8r‘s run-time type-checking.

The big problem with this approach is that Javascript’s introspection is weakest when examining functions: you pretty much need to parse the stringified function, and, even then, it’s often not obvious whether the function [always] returns a value or what kinds of parameters it expects. (Heck, TypeScript still can’t infer destructured parameter types,) Still, if I were going to cheat, it would be pretty easy to get ~75% of the way there using a bit of parsing. (E.g. assume that a function whose body includes a non-empty return statement returns something, and capture the parameters, inferring types from any default values and hoping parameter names are helpful.)

How do Monads fit into this?

A radically different approach that suggested itself was monads, a concept I have been tinkering with for some time. You could implement Monad classes and live in a world of “true monads” where all values are boxed with a wrapper instance, but that’s going to be tedious and ugly and have overhead. No, the idea is to provide monads as a convention and then make them easier to work with, or no harder to work with, than plain JavaScript.

The basic idea of monads is that:

  • Monads are pure functions without side effects. (For UI and I/O code, let’s say “pure-ish”.)
  • They take a single input (invariant) object, which may be an error object.
  • They produce a single output, which may be an error object.
  • If passed an error object, the monad simply passes it through.
  • An error object should indicate where the error occurred and why.

If you’re writing a monad by hand in Javascript, it would look something like this:

const defaults = { x: 3, y: 4 }
function vector2dLength(input = defaults) {
  const {x, y} = Object.assign({}, defaults, input)
  const output = {size: Math.sqrt(x * x + y * y)}
  return output
}

Compared to writing this in TypeScript you get to define your default parameters as a vanilla javascript object (that is available at runtime should you wish to introspect it). (In fact, you could simply compile this code as TypeScript and it would figure everything out.)

The amount of typing required is about the same as for TypeScript, and you get the benefits of autocompletion.

And you have monadic functions! You no longer need to worry about adding new parameters to functions or their outputs. You can chain function calls without having incomprehensible errors. You can provide a sane default value for any parameter. The one thing that’s problematic is optional parameters (i.e. parameters you want to have a type OR be absent).

Now, let’s suppose we create a helper function to create monads. Something along the lines of:

function monadic(inputDefault, outputDefault, func) {
  return input => func(Object.assign(
    {},
    inputDefault,
    input
  )
}

Writing monadic functions using this function is easier than the previous case, but TypeScript can’t infer the type. Now, we could write a clever TypeScript declaration for the function that could tell TypeScript that it emits a function whose parameter has the type of inputDefault, and whose output has the type of outputDefault.

This would be cute, but it still doesn’t address the problem of nullable types, and, like TypeScript, the type-checking is strictly static. Also note that neither of these toy examples implements the error pass-through property of monads.

However, the monadic function could instead look like this:

function monadic(inputDefault, outputDefault, func) {
  const monad = input => {
    if (input instanceof Error) {
      return Error
    }
    const inputErrors = matchType(inputDefault, input)
    if (inputErrors.length) {
      return new Error(inputErrors)
    }
    const output = func(Object.assign(
      {},
      inputDefault,
      input
    ))
    const outputErrors = matchType(outputDefault, output)
    if (outputErrors.length) {
      return new Error(outputErrors)
    }
    return output
  }
  monad.description = `
    (${describeType(inputType)}) =>
    ${describeType(outputDefault)}
  `
  return monad
}

In this example, describeType and matchTypes are utility functions from b8r‘s byExample library. describeType unambiguously describes the shape of a value it is passed, while matchType compares the descriptions of its arguments and provides an array of mismatches (so, if the array is empty, the shapes match).

The whole point of byExample is that a value describes its own type.

Now we can write the original example as:

const vector2dLength = monadic(
  {x: 3, y: 4},
  {size: 5}, 
  vec => { size: Math.sqrt(x * x + y * y) }
)

This produces a monad (it takes and emits a single value and passes through errors), which does runtime type-checking on its input and output (emitting errors as appropriate), and contains its own description which can be used to generate a .d.ts file (if so desired).

Now, if you’re a purist, you’ll have noticed that I haven’t implemented all the stuff you’re supposed to have when you implement proper monads. This is because, thanks to introspection, JavaScript functions (that accept and emit one value) satisfy the tests for function monads, and b8r’s describeType and matchTypes serve the role of maybe, etc. and JavaScript values satisfy the tests for value monads with no further work. (E.g. maybe for an object is written as foo instanceof Foo.) It’s just that all the virtues of monads will have to be implemented by you, by hand.

So, with all this we get self-documented, strictly-typed (at runtime) functions that short-circuit errors (so that no pointless computation occurs once an error occurs, and errors can be traced back to their origin. With a little extra work, I have made monadic itself a monad and implemented a convenient async case so that if you wrap an async function you get an async monad that awaits its input and its output before performing type-checks.

When you write this:

const vecLength = b8r.monadic({
  defaultIn: {x: 0, y: 0},
  defaultOut: {size: 0}, 
  func: async ({x, y}) => ({ size: Math.sqrt(x * x + y * y) })
})

The monadic version comes with its own description (or, if you’re doing static analysis, this can be computed from the code):

async ({
  "x": "number",
  "y": "number"
}) => {
  "size": "number"
}

You might note that this is pretty much a TypeScript type declaration. In fact, it’s finer-grained than TypeScript because describeType allows you to define very specific types, like cardinal numbers, that can be described by javascript (as strings) and tested at runtime.

The equivalent declaration in TypeScript looks like this (note the clumsy parameter declaration; destructured parameters are a bit of an Achilles heel for TypeScript (unless, having been annoying people for at least six years as of writing, it’s been fixed since I last checked.).

// TypeScript
const vecLength : (point: {x: number, y: number}) => number = point => {
  const {x, y} = point
  return {size: Math.sqrt(x * x + y * y)}
} 

OK, I may be biased, but I think the monadic declaration is easier to read and write. This gets my WIP type system very close to my “holy grail” for type-checking.

  • types are easy to read and write
  • typed code can run without transpilation
  • there is virtually no overhead to writing type-safe code (b8r already supports dynamic type-checking, and it’s built into the table-benchmark, which is why it has an optimization for huge lists).
  • types can be used as mocks, or to generate mocks (or even automate smoke tests)
  • the type system is within and part of JavaScript
  • types are available for static type-checking and autocomplete
  • types can be serialized (i.e. expressed as JSON), allowing self-documenting services
  • the type system is available at runtime

I could build a web-tech-based code editor (or possibly a vscode extension, but vscode extensions may be too sandboxed to do everything necessary) that asynchronously loads the module you’re working on, and, after a successful load, smoke-tests functions and grabs their descriptions to power autocomplete.

The next question is, how to migrate b8r itself over to the brave new world without breaking existing code. I have a plan as cunning as two weasels…