From 4313b923bf49e9370f16907f88352d3ce331e45c Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Wed, 3 Jul 2024 17:49:15 -0400 Subject: [PATCH] Refactor Option type to one implementation instead of two --- src/common/option.ts | 194 ++++++++++++++++--------------------------- 1 file changed, 73 insertions(+), 121 deletions(-) diff --git a/src/common/option.ts b/src/common/option.ts index fa4d0ce..252a5e9 100644 --- a/src/common/option.ts +++ b/src/common/option.ts @@ -1,175 +1,127 @@ +/** + * The sad, lonely enum that should be more tightly coupled + * to the Option type...but this isn't Rust + */ +enum OptionType { + Some = 'Some', + None = 'None', +} + +const isOption = (v: any): v is Option => v instanceof Option; + /** * Rust-style optional type * * Based on https://gist.github.com/s-panferov/575da5a7131c285c0539 */ -export default interface Option { - isSome(): boolean; - isNone(): boolean; - isSomeAnd(fn: (a: T) => boolean): boolean; - isNoneAnd(fn: () => boolean): boolean; - unwrap(): T | never; - unwrapOr(def: T): T; - unwrapOrElse(f: () => T): T; - map(f: (a: T) => U): Option; - mapOr(def: U, f: (a: T) => U): U; - mapOrElse(def: () => U, f: (a: T) => U): U; - and(optb: Option): Option; - andThen(f: (a: T) => Option): Option; - or(optb: Option): Option; - orElse(f: () => Option): Option; -} +export class Option { + /** + * The placeholder for the 'None' value type + */ + private static _noneInstance: Option = new Option(); -class _Some implements Option { - private value: T; + /** + * Is this a 'Some' or a 'None'? + */ + private optionType: OptionType; - constructor(v: T) { - this.value = v; - } + /** + * The value for the 'Some' type + */ + private value?: T; - static wrapNull(value: T): Option { - if (value == null) { - return None; + private constructor(v?: T | null) { + if (v !== undefined && v !== null) { + this.optionType = OptionType.Some; + this.value = v; } else { - return new _Some(value); + this.optionType = OptionType.None; + this.value = undefined; } } - map(fn: (a: T) => U): Option { - return new _Some(fn(this.value)); + public static get None(): Option { + return > Option._noneInstance; } - mapOr(_def: U, f: (a: T) => U): U { - return f(this.value); + public static Some(v: X): Option { + return new Option(v); } - mapOrElse(_def: () => U, f: (a: T) => U): U { - return f(this.value); + public static from(v: any): Option { + return (isOption(v)) ? Option.from(v.unwrap()) : new Option(v); } isSome(): boolean { - return true; + return this.optionType === OptionType.Some && this.value !== undefined; } isNone(): boolean { - return false; + return this.optionType === OptionType.None; } isSomeAnd(fn: (a: T) => boolean): boolean { - return fn(this.value); - } - - isNoneAnd(_fn: () => boolean): boolean { - return false; - } - - unwrap(): T { - return this.value; - } - - unwrapOr(_def: T): T { - return this.value; - } - - unwrapOrElse(_f: () => T): T { - return this.value; - } - - and(optb: Option): Option { - return optb; - } - - andThen(f: (a: T) => Option): Option { - return f(this.value); - } - - or(_optb: Option): Option { - return this; - } - - orElse(_f: () => Option): Option { - return this; - } - - toString(): string { - return 'Some ' + this.value; - } -} - -class _None implements Option { - constructor() { - } - - map(_fn: (a: T) => U): Option { - return > _None._instance; - } - - isSome(): boolean { - return false; - } - - isNone(): boolean { - return true; - } - - isSomeAnd(_fn: (a: T) => boolean): boolean { - return false; + return this.isSome() ? fn(this.unwrap()) : false; } isNoneAnd(fn: () => boolean): boolean { - return fn(); + return this.isNone() ? fn() : false; } - unwrap(): never { + map(fn: (a: T) => U): Option { + return this.isSome() ? new Option(fn(this.unwrap())) : Option._noneInstance; + } + + mapOr(def: U, f: (a: T) => U): U { + return this.isSome() ? f(this.unwrap()) : def; + } + + mapOrElse(def: () => U, f: (a: T) => U): U { + return this.isSome() ? f(this.unwrap()) : def(); + } + + unwrap(): T | never { + if (this.isSome() && this.value !== undefined) { + return this.value; + } + console.error('None.unwrap()'); throw 'None.get'; } unwrapOr(def: T): T { - return def; + return this.isSome() ? this.unwrap() : def; } unwrapOrElse(f: () => T): T { - return f(); + return this.isSome() ? this.unwrap() : f(); } - mapOr(def: U, _f: (a: T) => U): U { - return def; + and(optb: Option): Option { + return this.isSome() ? optb : Option._noneInstance; } - mapOrElse(def: () => U, _f: (a: T) => U): U { - return def(); - } - - and(_optb: Option): Option { - return _None.instance(); - } - - andThen(_f: (a: T) => Option): Option { - return _None.instance(); + andThen(f: (a: T) => Option): Option { + return this.isSome() ? f(this.unwrap()) : Option._noneInstance; } or(optb: Option): Option { - return optb; + return this.isNone() ? optb : this; } orElse(f: () => Option): Option { - return f(); + return this.isNone() ? f() : this; } - private static _instance: Option = new _None(); + toString(): string { + const innerValue = (this.value !== undefined) + ? JSON.stringify(this.value) + : ''; + const prefix = this.optionType.valueOf(); - public static instance(): Option { - return > _None._instance; - } - - public toString(): string { - return 'None'; + return (innerValue.length > 0) ? `${prefix} (${innerValue})` : prefix; } } -export const None: Option = _None.instance(); - -export function Some(value: T): Option { - return _Some.wrapNull(value); -} +export const { Some, None } = Option; +export default Option;