More Option refactoring, and some tests
All checks were successful
timw4mail/scroll/pipeline/head This commit looks good

This commit is contained in:
Timothy Warren 2024-07-03 19:09:04 -04:00
parent 4313b923bf
commit 090d6262c3
2 changed files with 58 additions and 36 deletions

View File

@ -3,7 +3,7 @@ import Buffer from './buffer.ts';
import Document from './document.ts'; import Document from './document.ts';
import Editor from './editor.ts'; import Editor from './editor.ts';
import { highlightToColor, HighlightType } from './highlight.ts'; import { highlightToColor, HighlightType } from './highlight.ts';
import _Option, { None, Some } from './option.ts'; import Option, { None, Some } from './option.ts';
import Position from './position.ts'; import Position from './position.ts';
import Row from './row.ts'; import Row from './row.ts';
@ -417,7 +417,19 @@ const EditorTest = {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
const OptionTest = { const OptionTest = {
// @TODO implement Option tests 'Option.from()': () => {
assertTrue(Option.from(null).isNone());
assertTrue(Option.from().isNone());
assertEquivalent(Option.from(undefined), None);
assertEquivalent(Option.from(Some('foo')), Some('foo'));
assertEquivalent(Some(Some('bar')), Some('bar'));
},
'.toString': () => {
assertEquals(Some({}).toString(), 'Some ({})');
assertEquals(Some([1, 2, 3]).toString(), 'Some ([1,2,3])');
assertEquals(None.toString(), 'None');
},
}; };
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------

View File

@ -7,8 +7,27 @@ enum OptionType {
None = 'None', None = 'None',
} }
// ----------------------------------------------------------------------------
// Typeguards to handle Some/None difference
// ----------------------------------------------------------------------------
const isOption = <T>(v: any): v is Option<T> => v instanceof Option; const isOption = <T>(v: any): v is Option<T> => v instanceof Option;
class OptionInnerNone<T> {
public type: OptionType = OptionType.None;
}
class OptionInnerSome<T> {
public type: OptionType = OptionType.Some;
constructor(public value: T) {}
}
type OptionInnerType<T> = OptionInnerNone<T> | OptionInnerSome<T>;
const isSome = <T>(v: OptionInnerType<T>): v is OptionInnerSome<T> =>
'value' in v && v.type === OptionType.Some;
/** /**
* Rust-style optional type * Rust-style optional type
* *
@ -18,50 +37,41 @@ export class Option<T> {
/** /**
* The placeholder for the 'None' value type * The placeholder for the 'None' value type
*/ */
private static _noneInstance: Option<any> = new Option(); private static _None: Option<any> = new Option(null);
/** /**
* Is this a 'Some' or a 'None'? * Is this a 'Some' or a 'None'?
*/ */
private optionType: OptionType; private readonly inner: OptionInnerType<T>;
/** private constructor(v?: T) {
* The value for the 'Some' type this.inner = (v !== undefined && v !== null)
*/ ? new OptionInnerSome(v)
private value?: T; : new OptionInnerNone();
private constructor(v?: T | null) {
if (v !== undefined && v !== null) {
this.optionType = OptionType.Some;
this.value = v;
} else {
this.optionType = OptionType.None;
this.value = undefined;
}
} }
public static get None(): Option<any> { public static get None(): Option<any> {
return <Option<any>> Option._noneInstance; return Option._None;
} }
public static Some<X>(v: X): Option<X> { public static Some<X>(v: any): Option<X> {
return new Option(v); return Option.from(v);
} }
public static from<X>(v: any): Option<X> { public static from<X>(v?: any): Option<X> {
return (isOption(v)) ? Option.from(v.unwrap()) : new Option(v); return (isOption(v)) ? Option.from(v.unwrap()) : new Option(v);
} }
isSome(): boolean { isSome(): boolean {
return this.optionType === OptionType.Some && this.value !== undefined; return isSome(this.inner);
} }
isNone(): boolean { isNone(): boolean {
return this.optionType === OptionType.None; return !this.isSome();
} }
isSomeAnd(fn: (a: T) => boolean): boolean { isSomeAnd(fn: (a: T) => boolean): boolean {
return this.isSome() ? fn(this.unwrap()) : false; return isSome(this.inner) ? fn(this.inner.value) : false;
} }
isNoneAnd(fn: () => boolean): boolean { isNoneAnd(fn: () => boolean): boolean {
@ -69,20 +79,20 @@ export class Option<T> {
} }
map<U>(fn: (a: T) => U): Option<U> { map<U>(fn: (a: T) => U): Option<U> {
return this.isSome() ? new Option(fn(this.unwrap())) : Option._noneInstance; return isSome(this.inner) ? Option.from(fn(this.inner.value)) : Option.None;
} }
mapOr<U>(def: U, f: (a: T) => U): U { mapOr<U>(def: U, f: (a: T) => U): U {
return this.isSome() ? f(this.unwrap()) : def; return isSome(this.inner) ? f(this.inner.value) : def;
} }
mapOrElse<U>(def: () => U, f: (a: T) => U): U { mapOrElse<U>(def: () => U, f: (a: T) => U): U {
return this.isSome() ? f(this.unwrap()) : def(); return isSome(this.inner) ? f(this.inner.value) : def();
} }
unwrap(): T | never { unwrap(): T | never {
if (this.isSome() && this.value !== undefined) { if (isSome(this.inner)) {
return this.value; return this.inner.value;
} }
console.error('None.unwrap()'); console.error('None.unwrap()');
@ -90,19 +100,19 @@ export class Option<T> {
} }
unwrapOr(def: T): T { unwrapOr(def: T): T {
return this.isSome() ? this.unwrap() : def; return isSome(this.inner) ? this.inner.value : def;
} }
unwrapOrElse(f: () => T): T { unwrapOrElse(f: () => T): T {
return this.isSome() ? this.unwrap() : f(); return isSome(this.inner) ? this.inner.value : f();
} }
and<U>(optb: Option<U>): Option<U> { and<U>(optb: Option<U>): Option<U> {
return this.isSome() ? optb : Option._noneInstance; return isSome(this.inner) ? optb : Option.None;
} }
andThen<U>(f: (a: T) => Option<U>): Option<U> { andThen<U>(f: (a: T) => Option<U>): Option<U> {
return this.isSome() ? f(this.unwrap()) : Option._noneInstance; return isSome(this.inner) ? f(this.inner.value) : Option.None;
} }
or(optb: Option<T>): Option<T> { or(optb: Option<T>): Option<T> {
@ -114,10 +124,10 @@ export class Option<T> {
} }
toString(): string { toString(): string {
const innerValue = (this.value !== undefined) const innerValue = (isSome(this.inner))
? JSON.stringify(this.value) ? JSON.stringify(this.inner.value)
: ''; : '';
const prefix = this.optionType.valueOf(); const prefix = this.inner.type.valueOf();
return (innerValue.length > 0) ? `${prefix} (${innerValue})` : prefix; return (innerValue.length > 0) ? `${prefix} (${innerValue})` : prefix;
} }