Skip to content

Instantly share code, notes, and snippets.

@hallettj
Last active March 4, 2024 07:05
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hallettj/d371a2246a9f776e4e4b2dbe760ece21 to your computer and use it in GitHub Desktop.
Save hallettj/d371a2246a9f776e4e4b2dbe760ece21 to your computer and use it in GitHub Desktop.
Sealed algebraic data type (ADT) in Javascript with Flow
/* @flow */
// Helper function for matching against an ADT.
export function match<A,B>(matcher: A): (match: (matcher: A) => B) => B {
return match => match(matcher)
}
/*
* A demonstration of how algebraic data types might be implemented using Flow.
*
* The `Entity` type have multiple possible shapes (`Player`, `Monster`, etc.).
* Flow will ensure that values of type `Entity` are constructed with values of
* the required types, and will ensure that functions that consume values of
* type `Entity` destructure those values and get the correct component types
* out.
*
* What makes this approach special is that Flow will ensure that functions that
* consume values of type `Entity` match against *all* possible shapes for the
* type. This helps to avoid problems were a new shape is added, but some
* functions are not updated to handle the new shape. In other words, the type
* `Entity` is *sealed*.
*
* @flow
*/
import { match } from './adt'
// The algebraic data type.
// This is the type that we use for entity values and function arguments.
export type Entity = <T>(_: EntityMatcher<T>) => T
// Type constructed by functions that match against the `Entity` type.
// This is the type that describes what an entity actually looks like.
// The matcher type does not necessarily have to be exported.
type EntityMatcher<T> = {
Player: (_: { health: number }) => T,
Monster: (_: { health: number, description: string }) => T,
Chest: (_: { contents: string[] }) => T,
Obstacle: (_: { description: string }) => T,
}
// Value constructors for the type `Entity`
export function Player(props: *): Entity {
return <T>(matcher: EntityMatcher<T>): T => matcher.Player(props)
}
export function Monster(props: *): Entity {
return <T>(matcher: EntityMatcher<T>): T => matcher.Monster(props)
}
export function Chest(props: *): Entity {
return <T>(matcher: EntityMatcher<T>): T => matcher.Chest(props)
}
export function Obstacle(props: *): Entity {
return <T>(matcher: EntityMatcher<T>): T => matcher.Obstacle(props)
}
// Examples of functions that consume or produce values of type `Entity`
const showEntity: (_: Entity) => string = match({
Player: ({ health }) => `our intrepid explorer (health: ${health})`,
Monster: ({ health, description }) => `${description} (health: ${health})`,
Chest: ({ contents }) => `the chest contains: ${contents.join(', ')}`,
Obstacle: ({ description }) => `${description} blocks the way`,
})
// Constructing an entity.
// Note that no type annotation is necessary.
const beagle = Monster({ description: "a ferocious beagle", health: 3 })
console.log(showEntity(beagle))
// Does not type-check, because not all branches are covered:
// const getHealth: (_: Entity) => number = match({
// Player: ({ health }) => health,
// Monster: ({ health }) => health,
// })
// Does not type-check, because `contents` and `description` are not numbers:
// const getHealth: (_: Entity) => number = match({
// Player: ({ health }) => health,
// Monster: ({ health }) => health,
// Chest: ({ contents }) => contents,
// Obstacle: ({ description }) => description,
// })
// Third time's the charm:
const getHealth: (_: Entity) => ?number = match({
Player: ({ health }) => health,
Monster: ({ health }) => health,
Chest: ({ contents }) => null,
Obstacle: ({ description }) => null,
})
console.log(getHealth(beagle))
// Type-checks correctly with no type annotations!
// Because we call this function with `beagle` as an argument later, Flow infers
// that the input type to this function is `Entity`, and infers that the output
// type is `boolean`.
const isMonster = match({
Player: () => false,
Monster: () => true,
Chest: () => false,
Obstacle: () => null,
})
console.log(isMonster(beagle))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment