Skip to content

Instantly share code, notes, and snippets.

@gcanti
Last active October 10, 2016 09:51
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gcanti/7695164287105e4f4a405d9c4ed71357 to your computer and use it in GitHub Desktop.
Save gcanti/7695164287105e4f4a405d9c4ed71357 to your computer and use it in GitHub Desktop.

Type driven development with Flow

Written by @alpacaaa and @GiulioCanti

"Type driven development" is a technique used to split a problem into a set of smaller problems, letting the type checker suggest the concrete implementation, or at least helping us getting there. Here's a practical example

The Problem

Say for instance that we like to reimplement the function Promise.all, we'll name it sequence. Let's start with its signature

declare function sequence<T>(promises: Array<Promise<T>>): Promise<Array<T>>;

Notice that a declared function, even if not yet implemented, is immediately available and can concur to type check the rest of our code. If we are running a build system though, we'll get an error because there's no such a function at runtime, for now it only exists in the "world of types".

However, the goal of this technique is working in the editor as long as possible without the need to run our code. We'll rely entirely on the type checker making sure it doesn't raise any errors as we go through.

Back to our problem, we need to transform (or "reduce") an array of values of type A to a value of type B. The transformation we're looking for is likely reduce, which in fact has the following signature

this: Array<A> ~> reduce<A, B>(f: (acc: B, x: A) => B, init: B): B

We don't know yet how to implement neither f nor init, but we can skip the concrete implementation as we did before. For now it's enough to simply declare the missing bits and replace the type parameters A and B with the corresponding types we're working on:

A = Promise<T>
B = Promise<Array<T>>

This is what we get

function sequence<T>(promises: Array<Promise<T>>): Promise<Array<T>> {
  // declaration
  declare function f(acc: Promise<Array<T>>, x: Promise<T>): Promise<Array<T>>;
  // declaration
  declare var init: Promise<Array<T>>;
  // implementation
  return promises.reduce(f, init)
}

Choosing init is straightforward, we can put an empty array into the Promise "container"

function sequence<T>(promises: Array<Promise<T>>): Promise<Array<T>> {
  declare function f(acc: Promise<Array<T>>, x: Promise<T>): Promise<Array<T>>;
  // implementation
  const init: Promise<Array<T>> = Promise.resolve([])
  return promises.reduce(f, init)
}

Implementing f is a more complicated, we know how to concat a value of type Array<T> with a value of type T to get another value of type Array<T>

function push<T>(x: Array<T>, y: T): Array<T> {
  return x.concat(y)
}

but how do we concat acc and x given that they are both promises? What we'd like to have is a procedure, let's name it liftA2, which can "lift" the function push producing a new function which can work on the values "inside" the promises. Again we just declare the expected result without any implementation

// declaration
declare function liftA2<A, B, C>(f: (a: A, b: B) => C): (a: Promise<A>, b: Promise<B>) => Promise<C>;

function sequence<T>(promises: Array<Promise<T>>): Promise<Array<T>> {
   // implementation
  function f(acc: Promise<Array<T>>, x: Promise<T>): Promise<Array<T>> {
    return liftA2(push)(acc, x)
  }
  const init: Promise<Array<T>> = Promise.resolve([])
  return promises.reduce(f, init)
}

Now we only need to implement liftA2

function liftA2<A, B, C>(f: (a: A, b: B) => C): (a: Promise<A>, b: Promise<B>) => Promise<C> {
  return (a, b) => a.then(aa => b.then(bb => f(aa, bb)))
}

and test the whole thing

function push<T>(x: Array<T>, y: T): Array<T> {
  return x.concat(y)
}

function liftA2<A, B, C>(f: (a: A, b: B) => C): (a: Promise<A>, b: Promise<B>) => Promise<C> {
  return (a, b) => a.then(aa => b.then(bb => f(aa, bb)))
}

function sequence<T>(promises: Array<Promise<T>>): Promise<Array<T>> {
  function f(acc: Promise<Array<T>>, x: Promise<T>): Promise<Array<T>> {
    return liftA2(push)(acc, x)
  }
  const init: Promise<Array<T>> = Promise.resolve([])
  return promises.reduce(f, init)
}

const promises: Array<Promise<number>> = [
  Promise.resolve(1),
  Promise.resolve(2),
  Promise.resolve(3)
]

sequence(promises).then(x => console.log(x)) // => [1, 2, 3]

As you can see a strong type system can not only prevent errors, but also guide you and provide feedback in your design process.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment