/** * This file contains the Free algebraic data type. Free is a data type that is * used primarily to create a Combinable for any given data structure. It is * useful when one wants to use combine things without deciding on a specific * data structure to implement. * * @experimental * @module Free * @since 2.0.0 */ import type { Kind, Out } from "./kind.ts"; import type { Combinable } from "./combinable.ts"; import { flow, pipe } from "./fn.ts"; /** * A Node represents a single value in the Free structure. * * @since 2.0.0 */ export type Node = { readonly tag: "Node"; readonly value: A; }; /** * A Link represents a combination of two Free structures. * * @since 2.0.0 */ export type Link = { readonly tag: "Link"; readonly first: Free; readonly second: Free; }; /** * The Free type represents either a Node (single value) or a Link (combination of two Free structures). * * @since 2.0.0 */ export type Free = Node | Link; /** * Specifies Free as a Higher Kinded Type, with covariant * parameter A corresponding to the 0th index of any substitutions. * * @since 2.0.0 */ export interface KindFree extends Kind { readonly kind: Free>; } /** * Create a Node containing a single value. * * @example * ```ts * import { node } from "./free.ts"; * * const singleValue = node(42); * console.log(singleValue); // { tag: "Node", value: 42 } * ``` * * @since 2.0.0 */ export function node(value: A): Free { return { tag: "Node", value }; } /** * Create a Link combining two Free structures. * * @example * ```ts * import { link, node } from "./free.ts"; * * const first = node(1); * const second = node(2); * const combined = link(first, second); * console.log(combined); // { tag: "Link", first: { tag: "Node", value: 1 }, second: { tag: "Node", value: 2 } } * ``` * * @since 2.0.0 */ export function link( first: Free, second: Free, ): Free { return { tag: "Link", first, second }; } /** * Check if a Free structure is a Node. * * @example * ```ts * import { isNode, node, link } from "./free.ts"; * * const single = node(1); * const combined = link(node(1), node(2)); * * console.log(isNode(single)); // true * console.log(isNode(combined)); // false * ``` * * @since 2.0.0 */ export function isNode(ua: Free): ua is Node { return ua.tag === "Node"; } /** * Check if a Free structure is a Link. * * @example * ```ts * import { isLink, node, link } from "./free.ts"; * * const single = node(1); * const combined = link(node(1), node(2)); * * console.log(isLink(single)); // false * console.log(isLink(combined)); // true * ``` * * @since 2.0.0 */ export function isLink(ua: Free): ua is Link { return ua.tag === "Link"; } /** * Pattern match on a Free structure to extract values. * * @example * ```ts * import { match, node, link } from "./free.ts"; * * const matcher = match( * (value) => `Single value: ${value}`, * (first, second) => `Combined: ${first} and ${second}` * ); * * const single = node(42); * const combined = link(node(1), node(2)); * * console.log(matcher(single)); // "Single value: 42" * console.log(matcher(combined)); // "Combined: [object Object] and [object Object]" * ``` * * @since 2.0.0 */ export function match( onNode: (value: A) => O, onLink: (first: Free, second: Free) => O, ): (ua: Free) => O { return (ua) => { switch (ua.tag) { case "Node": return onNode(ua.value); case "Link": return onLink(ua.first, ua.second); } }; } /** * Combine two Free structures. * * @example * ```ts * import { combine, node } from "./free.ts"; * import { pipe } from "./fn.ts"; * * const first = node(1); * const second = node(2); * const combined = pipe(first, combine(second)); * * console.log(combined.tag); // "Link" * ``` * * @since 2.0.0 */ export function combine( second: Free, ): (first: Free) => Free { return (first) => ({ tag: "Link", first, second }); } /** * Wrap a value in a Free structure. * * @example * ```ts * import { wrap } from "./free.ts"; * * const wrapped = wrap("Hello"); * console.log(wrapped); // { tag: "Node", value: "Hello" } * ``` * * @since 2.0.0 */ export function wrap(a: A): Free { return node(a); } /** * Apply a function to the value in a Free structure. * * @example * ```ts * import { map, node } from "./free.ts"; * import { pipe } from "./fn.ts"; * * const free = node(5); * const doubled = pipe( * free, * map(n => n * 2) * ); * * console.log(doubled); // { tag: "Node", value: 10 } * ``` * * @since 2.0.0 */ export function map( fai: (a: A) => I, ): (ua: Free) => Free { const go: (ua: Free) => Free = match( flow(fai, node), (first, second) => link(go(first), go(second)), ); return go; } /** * Chain Free computations together. * * @example * ```ts * import { flatmap, node } from "./free.ts"; * import { pipe } from "./fn.ts"; * * const free = node(5); * const chained = pipe( * free, * flatmap(n => node(n * 2)) * ); * * console.log(chained); // { tag: "Node", value: 10 } * ``` * * @since 2.0.0 */ export function flatmap( faui: (a: A) => Free, ): (ua: Free) => Free { const go: (ua: Free) => Free = match( faui, (first, second): Free => link(go(first), go(second)), ); return go; } /** * Apply a function wrapped in a Free to a value wrapped in a Free. * * @example * ```ts * import { apply, node } from "./free.ts"; * import { pipe } from "./fn.ts"; * * const freeFn = node((n: number) => n * 2); * const freeValue = node(5); * const result = pipe( * freeFn, * apply(freeValue) * ); * * console.log(result); // { tag: "Node", value: 10 } * ``` * * @since 2.0.0 */ export function apply(ua: Free): (ufai: Free<(a: A) => I>) => Free { return (ufai) => pipe(ufai, flatmap(flow(map, (fn) => fn(ua)))); } /** * Fold over a Free structure to produce a single value. * * @example * ```ts * import { fold, node, link } from "./free.ts"; * import { pipe } from "./fn.ts"; * * const free = link(node(1), node(2)); * const sum = pipe( * free, * fold( * (value, acc) => value + acc, * 0 * ) * ); * * console.log(sum); // 3 * ``` * * @since 2.0.0 */ export function fold( foldr: (value: A, accumulator: O) => O, initial: O, ): (ua: Free) => O { // :( let result = initial; const go: (ua: Free) => O = match( (value) => { result = foldr(value, result); return result; }, (first, second) => { go(first); return go(second); }, ); return go; } /** * Create a Combinable instance for Free. * * @example * ```ts * import { getCombinable, node } from "./free.ts"; * import { pipe } from "./fn.ts"; * * const combinable = getCombinable(); * const first = node(1); * const second = node(2); * const combined = pipe(first, combinable.combine(second)); * * console.log(combined.tag); // "Link" * ``` * * @since 2.0.0 */ export function getCombinable(): Combinable> { return { combine }; } // TODO: Showable, Sortable, Traversable //