Skip to content

Instantly share code, notes, and snippets.

@unscriptable
Last active January 8, 2016 16:54
Show Gist options
  • Save unscriptable/114aa624f53959c1fb64 to your computer and use it in GitHub Desktop.
Save unscriptable/114aa624f53959c1fb64 to your computer and use it in GitHub Desktop.
A typesafe shopping cart in flowtype (see http://flowtype.org)
'use strict';
/* @flow */
// A typesafe shopping cart in flowtype.
// Flow relies heavily on EcmaScript constructs, primarily `class` to
// define types. This feels quite restrictive if you've been using a strongly
// typed language, such as Haskell. Still, I managed to build this "safe"
// suite of shopping cart functions that prevents the developer from doing
// nonsensical things with the cart. For instance, you can't compile code
// that attempts to pay for an empty cart and you can't remove or replace
// items from an empty or paid cart.
// TODO: use immutable data structures!
// Cart items.
type SKU = string;
type Item = {
sku: SKU,
quantity: number
};
type Items = Map<SKU, Item>;
// Cart states.
// In lieu of enums, the best solution I've got so far is this
// set of `class` types.
class Empty {}
class Loaded {}
class Paid extends Loaded {}
// These instances help clarify the code and improve performance (see below).
const empty = new Empty();
const loaded = new Loaded();
const paid = new Paid();
// Cart type.
// A Cart is parameterized by state. This is what makes it "safe". Cart is not
// exported so users can't bypass the safety encoded in the functions below.
class Cart<state: Empty | Loaded | Paid> {
// Since this implementation relies on `class`, all cart flavors have the
// same constructor. Thanks to "Maybe types" (note the `?Items`), we can
// optionally omit the cart items for an empty cart. As far as I can tell,
// flow can't protect the code author from providing an empty array, though.
constructor (state: Empty | Loaded | Paid, items: ?Items) {
this.items = items != null ? items : new Map();
}
items: Items;
}
// Exported cart functions. These functions prevents many logic errors
// *at compile time* (yey), rather than run time!
// Note that the format of these functions allow us to place the type
// definition on a separate line! This is important for readability, but is
// also criticial imho for proper reasoning. Types *should* be independent
// of implementation, but flow tries to conflate them. Step 1: stop
// requiring parameter names in the type definitions! :)
// Create a new (empty!) shopping cart.
export const create
: () => Cart<Empty>
= () => new Cart(empty);
// Add an item to a cart that isn't in the paid state.
export const addItem
: (cart: Cart<Empty> | Cart<Loaded>, item: Item) => Cart<Loaded>
= (cart, item) => {
const items = cart.items;
if (items.has(item.sku)) {
throw new Error(`Item is already in cart: ${ item.sku }.`);
}
items.set(item.sku, item);
return new Cart(loaded, items);
};
// Remove an item from a non-empty, non-paid cart. Note: you may receive a
// loaded or an empty cart back, so your code *must* branch after this call
// or flow will fail to compile your code!
export const removeItem
: (cart: Cart<Loaded>, item: Item) => Cart<Loaded> | Cart<Empty>
= (cart, item) => {
const items = cart.items;
if (!items.has(item.sku)) {
throw new Error(`Item is not in cart: ${ item.sku }.`);
}
items.delete(item.sku);
return items.count === 0
? new Cart(empty)
: new Cart(loaded, items);
};
// Replace an item in a non-empty, non-paid cart.
export const replaceItem
: (cart: Cart<Loaded>, item: Item) => Cart<Loaded>
= (cart, item) => {
const items = cart.items;
if (!items.has(item.sku)) {
throw new Error(`Item is not in cart: ${ item.sku }.`);
}
items.set(item.sku);
return new Cart(loaded, items);
};
// Pay for a non-empty, non-paid cart.
export const pay
: (cart: Cart<Loaded>) => Cart<Paid>
= (cart) => {
// do some payments stuff here
return new Cart(paid, cart.items);
};
// Some example code:
let cart = create(); // cart is Cart<Empty>
cart = addItem(cart, { sku: '123213', quantity: 2 }); // cart is Cart<Loaded>
const paidCart = pay(cart);
// Some example code that does *not* compile.
// this line should fail to type-check since you can't call `removeItem` with
// an empty cart (removeItem does not accept `Cart<Empty>`):
cart = removeItem(create(), { sku: '123213', quantity: 2 });
@spion
Copy link

spion commented Jan 8, 2016

@spion
Copy link

spion commented Jan 8, 2016

Playground link: http://goo.gl/5MGRhf

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