The Eff monad implemented in Flow

Adapted from “Supercharged Types” (http://rtpg.co/2016/07/20/supercharged-types.html) by @rtpg_

Flow has Row Polymorphism, an important feature if you want to encode invariants into types. For people with Haskell experience, Eff is kinda like IO, things with side effects end up in Eff (PureScript users should feel comfortable).

Here’s a tiny example. I’m not going to explain too much about the type mechanisms themselves, just a taste of what is possible.

type DB = { type: 'DB' };
type User = {
username: string,
uid: number
};
function createUser(username: string): Eff<{ write: DB }, User> {

}
function lookupUser(username: string): Eff<{ read: DB }, ?User> {

}

  • the type DB represents a side effect
  • the type of createUser is (username: string) => Eff<{ write: DB }, User>. It means that createUser writes to the DB and gives you a User back
  • the type of lookupUser is similar: (username: string) => Eff<{ read: DB }, ?User>. Given a string (in our case a username), it will return an action that will read from the DB and return a User (if found).

And here’s a function that will create a user and then look it up

const createThenLookupUser = username => createUser(username)
.chain(user => lookupUser(user.username))

What’s the type of createThenLookupUser? Let’s ask Flow!

$> flow suggest index.js
const createThenLookupUser = username => createUser(username).chain(user: {uid: number, username: string} => lookupUser(user.uid): Eff<{read: {type: ‘DB’}}, ?{uid: number, username: string}>): Eff<{write: {type: ‘DB’}} & {read: {type: ‘DB’}}, ?{uid: number, username: string}>

Flow is pretty verbose but if you skip the cruft you can see


(username: string) => Eff<{ write: DB, read: DB }, ?User>

The type inference figured out that createThenLookupUser

- writes to the DB (write: DB)
- reads from the DB (read: DB)

Row polymorphism here is used to encode effects. We can combine actions within Eff, and the type system will accumulate the effects and keep track of them.

Whitelisting side effects

Let’s see how you can whitelist the effects returned by a function. I’m going to write a model for a (server side) router.

First some types

type Method = 'GET' | 'POST';
// web request
type Req = {
body: string,
header: string,
method: Method
};
// web response
type Res = {
body: string,
status: number
};

Second, let’s write an endpoint to register to our service

function signupPage(req: Req): Eff<{ write: DB, read: DB }, Res> {
const username = req.header
return lookupUser(username).chain(user => {
if (user) {
return new Eff(() => ({
body : 'A user with this username already exists!',
status : 400
}))
}
return createUser(username).map(() => ({
body : 'Created User with name ' + username,
status: 200
}))
})
}

Now let’s write the router. A Route is either a GET or a POST endpoint. We want to enforce that GET endpoints can’t write to the db

type GetRoute = {
type: ‘GetRoute’,
path: string,
handler: (req: Req) => Eff<{ read: DB }, Res>
};
type PostRoute = {
type: ‘PostRoute’,
path: string,
handler: (req: Req) => Eff<{ read: DB, write: DB }, Res>
};
type Route = GetRoute | PostRoute;

Finally our main routes


const routes: Array<Route> = [
{ type: 'GetRoute', path: '/signup', handler: signupPage }
]

But if you run Flow you get the following error

{ type: 'GetRoute', path: '/signup', handler: signupPage }
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This type is incompatible with union: GetRoute | PostRoute
property `write`. Property not found in
handler: (req: Req) => Eff<{ read: DB }, Res>
^^^^^^^^^^^^ object type

This is great. signupPage totally writes to the DB! And GETs should not be allowed to change the server state.

Changing the route definition to

const routes: Array<Route> = [
{ type: 'PostRoute', path: '/signup', handler: signupPage }
]

solves the problem: the endpoint is now allowed to write to the DB, because of the types of PostRoute and signupPage.

The Eff monad