Last active
March 4, 2024 07:05
-
-
Save hallettj/d371a2246a9f776e4e4b2dbe760ece21 to your computer and use it in GitHub Desktop.
Sealed algebraic data type (ADT) in Javascript with Flow
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* @flow */ | |
// Helper function for matching against an ADT. | |
export function match<A,B>(matcher: A): (match: (matcher: A) => B) => B { | |
return match => match(matcher) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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