diff --git a/.idea/deno.xml b/.idea/deno.xml index 42c1442..8df8236 100644 --- a/.idea/deno.xml +++ b/.idea/deno.xml @@ -1,7 +1,7 @@ - \ No newline at end of file diff --git a/deno.jsonc b/deno.jsonc index 49115ad..f42153f 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,6 +1,7 @@ { "name": "@suddenly-giovanni/std", "version": "0.0.4", + "lock": true, "exports": { "./predicate": "./src/predicate/mod.ts", "./option": "./src/option/mod.ts" diff --git a/deno.lock b/deno.lock index cc43d20..72ef3ec 100644 --- a/deno.lock +++ b/deno.lock @@ -3,6 +3,8 @@ "packages": { "specifiers": { "jsr:@std/assert": "jsr:@std/assert@0.225.3", + "jsr:@std/assert@^0.225.3": "jsr:@std/assert@0.225.3", + "jsr:@std/expect": "jsr:@std/expect@0.224.2", "jsr:@std/internal@^1.0.0": "jsr:@std/internal@1.0.0", "jsr:@std/testing": "jsr:@std/testing@0.224.0", "npm:@biomejs/biome": "npm:@biomejs/biome@1.7.3", @@ -15,6 +17,13 @@ "jsr:@std/internal@^1.0.0" ] }, + "@std/expect@0.224.2": { + "integrity": "789ba764319503b1669b54fad2d279f35fe708247548878c5fb44e0cacc40a8e", + "dependencies": [ + "jsr:@std/assert@^0.225.3", + "jsr:@std/internal@^1.0.0" + ] + }, "@std/internal@1.0.0": { "integrity": "ac6a6dfebf838582c4b4f61a6907374e27e05bedb6ce276e0f1608fe84e7cd9a" }, diff --git a/src/internal/function.ts b/src/internal/function.ts index 27d3a64..3462759 100644 --- a/src/internal/function.ts +++ b/src/internal/function.ts @@ -1,5 +1,32 @@ // deno-lint-ignore-file ban-types +/** + * A lazy computation that can be used to defer the evaluation of an expression. + * + * @example + * To defer the evaluation of an expression: + * ```ts + * import { type Lazy } from './function.ts' + * declare function fib(n: number): number + * + * const lazy: Lazy = () => fib(100) // fib is not called yet, hence the computation is deferred + * ``` + * + * @example + * As a parameter type in a function: + * ```ts + * import { type Lazy } from './function.ts' + * function defer(f: Lazy): A { + * return f() + * } + * const value = defer(() => 1) // 1 + * ``` + */ +export interface Lazy { + // biome-ignore lint/style/useShorthandFunctionType: we want the opaque type here + (): A +} + /** * Pipes the value of an expression into a pipeline of functions. * diff --git a/src/internal/types.test.ts b/src/internal/types.test.ts new file mode 100644 index 0000000..67056ec --- /dev/null +++ b/src/internal/types.test.ts @@ -0,0 +1,23 @@ +import { expectTypeOf } from 'npm:expect-type@0.19.0' +import { describe, test } from 'jsr:@std/testing/bdd' +import type { Types } from './types.ts' + +describe('Types', () => { + test('Tag', () => { + expectTypeOf & unknown>().toEqualTypeOf< + 'a' | 'b' + >() + }) + + test('Equal', () => { + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + }) + + test('Simplify', () => { + expectTypeOf>().toEqualTypeOf<{ + a: number + b: number + }>() + }) +}) diff --git a/src/internal/types.ts b/src/internal/types.ts new file mode 100644 index 0000000..7abaea4 --- /dev/null +++ b/src/internal/types.ts @@ -0,0 +1,66 @@ +/** + * This namespace provides utility types for type manipulation. + * + * @example + * ```ts + * import type { Types } from './types.ts' + * + * type Test1 = Types.Tags + * // ^? "a" | "b" + * + * type Test2 = Types.Equals<{ a: number }, { a: number }> + * // ^? true + * + * type Test3 = Types.Simplify<{ a: number } & { b: number }> + * // ^? { a: number; b: number; } + * + * ``` + */ +export declare namespace Types { + /** + * Returns the tags in a type. + * + * @example + * ```ts + * import type { Types } from './types.ts' + * + * type Res = Types.Tags // "a" | "b" + * ``` + * + * @category types + */ + export type Tags = E extends { _tag: string } ? E['_tag'] : never + + /** + * Determines if two types are equal. + * + * @example + * ```ts + * import type { Types } from './types.ts' + * + * type Res1 = Types.Equals<{ a: number }, { a: number }> // true + * type Res2 = Types.Equals<{ a: number }, { b: number }> // false + * ``` + * + * @category models + */ + export type Equals = (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 + ? true + : false + + /** + * Simplifies the type signature of a type. + * + * @example + * ```ts + * import type { Types } from './types.ts' + * + * type Res = Types.Simplify<{ a: number } & { b: number }> // { a: number; b: number; } + * ``` + * @category types + */ + export type Simplify = { + [K in keyof A]: A[K] + } extends infer B ? B + : never +} diff --git a/src/option/mod.ts b/src/option/mod.ts index eb94524..f3f19cc 100644 --- a/src/option/mod.ts +++ b/src/option/mod.ts @@ -1 +1,44 @@ +/** + * # Option Module + * The Option Module is inspired by the Scala 3 Option type. This module primarily solves one of the common problems in programming - avoiding null and undefined values. + * + * The Option type is a container that encapsulates an optional value, i.e., it stores some value or none. It's a safer alternative to using null or undefined. By using Option, you can avoid null reference errors. The main advantage of using an `Option` type is that you're explicitly dealing with something that may or may not be there. + * + * ## How it works + * This module exports a base abstract class `Option` and two concrete subclasses `Some` and `None`. An `Option` object encapsulates a value that may or may not be present. A `Some` object represents an `Option` that contains a value, and `None` represents an `Option` that has no value. + * + * ## How to use it + * When you have a value that you want to lift into a boxed `Option`, you create an object of type `Some`, as follows: + * + * ```ts + * import { Option } from './option.ts' + * + * const value: Option.Type = Option.Some("Hello, world!"); + * ``` + * + * If there isn't a value to lift, you create a `None` object, as follows: + * + * ```ts + * import { Option } from './option.ts' + * + * const none: Option.Type = Option.None(); + * ``` + * + * To check the contents of an `Option`, use the `isSome` and `isNone` methods and extract the value when it is `Some`: + * + * ```ts + * import { Option } from './option.ts' + * + * const none: Option.Type = Option.None(); + * + * if (none.isSome()) { + * console.log(none.get()); // this will throw an error !!! + * } else { + * console.log("Value is None"); + * } + * ``` + * + * @module + */ + export { Option } from './option.ts' diff --git a/src/option/option.test.ts b/src/option/option.test.ts index 6ba7be3..c688e48 100644 --- a/src/option/option.test.ts +++ b/src/option/option.test.ts @@ -1,82 +1,275 @@ -import { assert, assertEquals, assertStrictEquals, assertThrows, equal } from 'jsr:@std/assert' +import { expectTypeOf } from 'npm:expect-type@0.19.0' +import { assertEquals, equal } from 'jsr:@std/assert' +import { expect } from 'jsr:@std/expect' import { describe, it, test } from 'jsr:@std/testing/bdd' + +import { pipe } from '../internal/function.ts' +import type * as F from '../internal/function.ts' import { Option } from './option.ts' +// deno-lint-ignore no-namespace +namespace Util { + // biome-ignore lint/suspicious/noExportsInTest: + export const deepStrictEqual = (actual: A, expected: A) => { + assertEquals(actual, expected) + } +} + describe('Option', () => { describe('constructors', () => { - it('should throw an error when trying to construct Option directly ', () => { - assertThrows(() => { + test('should throw an error when trying to construct Option directly ', () => { + expect(() => { // @ts-expect-error - TSC does not allow instantiation of abstract classes, but what about runtime? new Option() - }) + }).toThrow('Option is not meant to be instantiated directly') }) - it('Some', () => { + test('Some', () => { const some = Option.Some(1) - assertStrictEquals(some._tag, 'Some') - assertStrictEquals(some.value, 1) + expect(some._tag).toBe('Some') + expect(some.get()).toBe(1) }) describe('None', () => { - it('constructor', () => { + test('constructor', () => { const none = Option.None() - assertStrictEquals(none._tag, 'None') + expect(none._tag).toBe('None') }) - it('None is a singleton ', () => { + test('None is a singleton ', () => { const none = Option.None() - assert(Option.None() === none) - assert(Object.is(Option.None(), Option.None())) + expect(Option.None()).toStrictEqual(none) + expect(Object.is(Option.None(), Option.None())).toBe(true) + }) + }) + + test('fromNullable', () => { + expect(Option.fromNullable(2).equals(Option.Some(2))).toBe(true) + expectTypeOf(Option.fromNullable(2)).toEqualTypeOf>() + + expect(Option.fromNullable(0).equals(Option.Some(0))).toBe(true) + expectTypeOf(Option.fromNullable(0)).toEqualTypeOf>() + + expect(Option.fromNullable('').equals(Option.Some(''))).toBe(true) + expectTypeOf(Option.fromNullable('')).toEqualTypeOf>() + + expect(Option.fromNullable([]).equals(Option.Some([]), equal)).toBe(true) + expectTypeOf(Option.fromNullable([])).toEqualTypeOf>() + expectTypeOf(Option.fromNullable(['foo'])).toEqualTypeOf>() + + const nullOption = Option.fromNullable(null) + expect(nullOption.isNone()).toBe(true) + expectTypeOf(nullOption).toEqualTypeOf>() + + const undefinedOption = Option.fromNullable(undefined) + expect(undefinedOption.isNone()).toBe(true) + expectTypeOf(undefinedOption).toEqualTypeOf>() + + interface Bar { + readonly baz?: null | number + } + + expectTypeOf(Option.fromNullable({} as undefined | Bar)).toEqualTypeOf< + Option.Type<{ + readonly baz?: null | number + }> + >() + expectTypeOf(Option.fromNullable({} as Bar)).toEqualTypeOf< + Option.Type<{ + readonly baz?: null | number + }> + >() + expectTypeOf(Option.fromNullable(undefined as undefined | string)).toEqualTypeOf< + Option.Type + >() + expectTypeOf(Option.fromNullable(null as null | number)).toEqualTypeOf< + Option.Type + >() + }) + }) + + describe('Option type namespace', () => { + test('Option.Value', () => { + const someOfNumber = Option.Some(1) + expectTypeOf>().toEqualTypeOf() + + const noneOfUnknown = Option.None() + const noneOfNumber = Option.None() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + + type Foo = { foo: string } + const someOfRecord = Option.Some({ foo: 'bar' } satisfies Foo) + expectTypeOf>().toEqualTypeOf() + }) + }) + + describe('get', () => { + test('Some', () => { + expect(Option.Some(1).get()).toBe(1) + expect(Option.Some({ foo: 'bar' }).get()).toEqual({ foo: 'bar' }) + }) + + test('None', () => { + expect(() => Option.None().get()).toThrow('None.get') + }) + }) + + describe('getOrElse', () => { + test('Some', () => { + expect(Option.Some(1).getOrElse(() => 2)).toBe(1) + expect(Option.Some({ foo: 'bar' }).getOrElse(() => ({ foo: 'baz' }))).toEqual({ + foo: 'bar', + }) + + expectTypeOf(Option.Some({ foo: 'bar' }).getOrElse(() => ({ foo: 'baz' }))) + .toEqualTypeOf<{ + foo: string + }>() + + expectTypeOf( + Option.Some({ foo: 'bar' }).getOrElse(() => ({ foo: 'baz' })), + ).not.toEqualTypeOf<{ foo: number }>() // it should extend the type Option instance type + }) + + test('None', () => { + expect(Option.None().getOrElse(() => 2)).toBe(2) + expect( + Option.fromNullable(undefined).getOrElse(() => ({ + foo: 'baz', + })), + ).toEqual({ + foo: 'baz', }) + expect(Option.None().getOrElse(() => null)).toEqual(null) + }) + }) + + describe('fold', () => { + const fa = (s: string): { length: number } => ({ length: s.length }) + const ifEmpty: F.Lazy = () => 42 + + it('returns call the ifEmpty for None cases ', () => { + const stringOption = Option.fromNullable(null) + + // test the instance method + Util.deepStrictEqual( + stringOption.fold(ifEmpty, (_) => { + throw new Error('Called `absurd` function which should be un-callable') + }), + 42, + ) + + expectTypeOf( + stringOption.fold(ifEmpty, (_): boolean => { + throw new Error('Called `absurd` function which should be un-callable') + }), + ).toEqualTypeOf() + + // test the static method + Util.deepStrictEqual( + pipe( + null as null | string, + Option.fromNullable, + Option.fold(ifEmpty, (_) => { + throw new Error('Called `absurd` function which should be un-callable') + }), + ), + 42, + ) + + expectTypeOf( + pipe( + stringOption, + Option.fold(ifEmpty, (_) => { + throw new Error('Called `absurd` function which should be un-callable') + }), + ), + ).toEqualTypeOf(42) + }) + + it('should call `f` for the `Some` case', () => { + const stringOption = Option.fromNullable('abc') + // test the instance method + Util.deepStrictEqual(stringOption.fold(ifEmpty, fa), { length: 3 }) + expectTypeOf(stringOption.fold(ifEmpty, fa)).toEqualTypeOf< + number | { length: number } + >() + + // test the static method + Util.deepStrictEqual( + pipe('abc' as null | string, Option.fromNullable, Option.fold(ifEmpty, fa)), + { length: 3 }, + ) + expectTypeOf(Option.fold(ifEmpty, fa)(stringOption)).toEqualTypeOf< + number | { length: number } + >() }) + }) - it('fromNullable', () => { - assertStrictEquals(Option.fromNullable(2).equals(Option.Some(2)), true) - assertStrictEquals(Option.fromNullable(0).equals(Option.Some(0)), true) - assertStrictEquals(Option.fromNullable('').equals(Option.Some('')), true) - assertStrictEquals(Option.fromNullable([]).equals(Option.Some([]), equal), true) + describe('match', () => { + const onNone = () => 'none' as const + const onSome = (s: string): number => s.length + test('static', () => { + Util.deepStrictEqual( + pipe(null as null | string, Option.fromNullable, Option.match({ onNone, onSome })), + 'none', + ) + Util.deepStrictEqual(pipe('abc', Option.Some, Option.match({ onNone, onSome })), 3) - assertStrictEquals(Option.isNone(Option.fromNullable(null)), true) - assertStrictEquals(Option.fromNullable(undefined).isNone(), true) + expectTypeOf(Option.match({ onNone, onSome })(Option.fromNullable(null))).toEqualTypeOf< + number | 'none' + >() + }) + test('instance', () => { + Util.deepStrictEqual(Option.None().match({ onNone, onSome }), 'none') + Util.deepStrictEqual(Option.Some('abc').match({ onNone, onSome }), 3) + expectTypeOf(Option.fromNullable(null).match({ onNone, onSome })).toEqualTypeOf< + number | 'none' + >() }) }) describe('guards', () => { - it('isOption', () => { - assertStrictEquals(Option.isOption(Option.Some(1)), true) - assertStrictEquals(Option.isOption(Option.None()), true) - assertStrictEquals(Option.isOption({}), false) + test('isOption', () => { + expect(pipe(Option.Some(1), Option.isOption)).toBe(true) + expect(pipe(Option.None(), Option.isOption)).toBe(true) + expect(pipe({}, Option.isOption)).toBe(false) }) describe('isNone', () => { test('on Option static method ', () => { - assertStrictEquals(Option.isNone(Option.None()), true) - assertStrictEquals(Option.isNone(Option.Some(1)), false) + expect(pipe(Option.None(), Option.isNone)).toBe(true) + expect(pipe(Option.Some(1), Option.isNone)).toBe(false) }) test('on Option instances: Some and None ', () => { - assertStrictEquals(Option.fromNullable(42).isNone(), false) - assertStrictEquals(Option.fromNullable(undefined).isNone(), true) + expect(Option.fromNullable(42).isNone()).toBe(false) + expect(Option.fromNullable(undefined).isNone()).toBe(true) }) }) + test('isEmpty', () => { + expect(Option.None().isEmpty()).toBe(true) + expect(Option.Some('foo').isEmpty()).toBe(false) + }) + describe('isSome', () => { test('on Option static method ', () => { - assertStrictEquals(Option.isSome(Option.None()), false) - assertStrictEquals(Option.isSome(Option.Some(1)), true) + expect(pipe(Option.None(), Option.isSome)).toBe(false) + expect(pipe(Option.Some(1), Option.isSome)).toEqual(true) }) test('on Option instances: Some and None ', () => { - assertStrictEquals(Option.fromNullable(null).isSome(), false) - assertStrictEquals(Option.fromNullable(0).isSome(), true) + expect(Option.fromNullable(null).isSome()).toBe(false) + expect(Option.fromNullable(0).isSome()).toBe(true) }) }) }) describe('serialize', () => { - it('toStringTag', () => { - assertStrictEquals( - String(Option.Some(1)), + test('toStringTag', () => { + expect(String(Option.Some(1))).toBe( JSON.stringify( { _id: 'Option', @@ -87,8 +280,7 @@ describe('Option', () => { 2, ), ) - assertStrictEquals( - String(Option.None()), + expect(String(Option.None())).toBe( JSON.stringify( { _id: 'Option', @@ -102,28 +294,28 @@ describe('Option', () => { }) describe('Equal', () => { - it('None ', () => { - assertStrictEquals(Option.None().equals(Option.None()), true) - assertStrictEquals(Option.None().equals(Option.Some('a')), false) + test('None ', () => { + expect(Option.None().equals(Option.None())).toBe(true) + expect(Option.None().equals(Option.Some('a'))).toBe(false) }) - it('Some', () => { - assertStrictEquals(Option.Some('a').equals(Option.None()), false) - assertStrictEquals(Option.Some(1).equals(Option.Some(2)), false) - assertStrictEquals(Option.Some(1).equals(Option.Some(1)), true) + test('Some', () => { + expect(Option.Some('a').equals(Option.None())).toBe(false) + expect(Option.Some(1).equals(Option.Some(2))).toBe(false) + expect(Option.Some(1).equals(Option.Some(1))).toBe(true) - assertStrictEquals(Option.Some([]).equals(Option.Some([])), false) - assertStrictEquals(Option.Some([]).equals(Option.Some([])), false) + expect(Option.Some([]).equals(Option.Some([]))).toBe(false) + expect(Option.Some([]).equals(Option.Some([]))).toBe(false) const arr = [1, 2, 3] - assertStrictEquals(Option.Some(arr).equals(Option.Some(arr)), true) - assertStrictEquals(Option.Some(arr).equals(Option.Some([1, 2, 3])), false) + expect(Option.Some(arr).equals(Option.Some(arr))).toBe(true) + expect(Option.Some(arr).equals(Option.Some([1, 2, 3]))).toBe(false) }) - it('should use the custom comparison predicate strategy', () => { - assertStrictEquals(Option.Some([]).equals(Option.Some([]), equal), true) - assertStrictEquals(Option.Some([1, 2, 3]).equals(Option.Some([1, 2, 3]), equal), true) - assertStrictEquals( + test('should use the custom comparison predicate strategy', () => { + expect(Option.Some([]).equals(Option.Some([]), equal)).toBe(true) + expect(Option.Some([1, 2, 3]).equals(Option.Some([1, 2, 3]), equal)).toBe(true) + expect( Option.Some({ foo: { bar: { @@ -140,35 +332,35 @@ describe('Option', () => { }), equal, ), - true, - ) + ).toBe(true) }) }) describe('Inspectable', () => { const some = Option.Some(1) const none = Option.None() - it('toStringTag', () => { - assertStrictEquals(some[Symbol.toStringTag], 'Option.Some') - assertStrictEquals(none[Symbol.toStringTag], 'Option.None') + + test('toStringTag', () => { + expect(some[Symbol.toStringTag]).toBe('Option.Some') + expect(none[Symbol.toStringTag]).toBe('Option.None') }) - it('toJSON', () => { - assertEquals(some.toJSON(), { + test('toJSON', () => { + expect(some.toJSON()).toEqual({ _id: 'Option', _tag: 'Some', value: 1, }) - assertEquals(none.toJSON(), { + expect(none.toJSON()).toEqual({ _id: 'Option', _tag: 'None', }) }) - it('toString', () => { - assertStrictEquals(some.toString(), JSON.stringify(some.toJSON(), null, 2)) - assertStrictEquals(none.toString(), JSON.stringify(none.toJSON(), null, 2)) + test('toString', () => { + expect(some.toString()).toBe(JSON.stringify(some.toJSON(), null, 2)) + expect(none.toString()).toBe(JSON.stringify(none.toJSON(), null, 2)) }) }) }) diff --git a/src/option/option.ts b/src/option/option.ts index 8831505..aa48f76 100644 --- a/src/option/option.ts +++ b/src/option/option.ts @@ -1,47 +1,5 @@ -/** - * # Option Module - * The Option Module is inspired by the Scala 3 Option type. This module primarily solves one of the common problems in programming - avoiding null and undefined values. - * - * The Option type is a container that encapsulates an optional value, i.e., it stores some value or none. It's a safer alternative to using null or undefined. By using Option, you can avoid null reference errors. The main advantage of using an `Option` type is that you're explicitly dealing with something that may or may not be there. - * - * ## How it works - * This module exports a base abstract class `Option` and two concrete subclasses `Some` and `None`. An `Option` object encapsulates a value that may or may not be present. A `Some` object represents an `Option` that contains a value, and `None` represents an `Option` that has no value. - * - * ## How to use it - * When you have a value that you want to lift into a boxed `Option`, you create an object of type `Some`, as follows: - * - * ```ts - * import { Option } from './option.ts' - * - * const value = Option.Some("Hello, world!"); - * ``` - * - * If there isn't a value to lift, you create a `None` object, as follows: - * - * ```ts - * import { Option } from './option.ts' - * - * const value = Option.None(); - * ``` - * - * To check the contents of an `Option`, use the `isSome` and `isNone` methods and extract the value when it is `Some`: - * - * ```ts - * import { Option } from './option.ts' - * - * const value = Option.None(); - * - * if (value.isSome()) { - * console.log(value.value); - * } else { - * console.log("Value is None"); - * } - * ``` - * - * @module - */ - import type { Equals } from '../internal/equals.ts' +import type * as F from '../internal/function.ts' import type { Inspectable } from '../internal/inspectable.ts' function format(x: unknown): string { @@ -54,12 +12,12 @@ function format(x: unknown): string { * The most idiomatic way to use an Option instance is to treat it as monad and use `map`,`flatMap`,` filter`, or `foreach`: * These are useful methods that exist for both Some and None: * -[ ] `isDefined` : True if not empty - * -[ ] `isEmpty` : True if empty + * - {@linkcode Option#isEmpty} : True if empty * -[ ] `nonEmpty`: True if not empty * -[ ] `orElse`: Evaluate and return alternate optional value if empty * -[ ] `getOrElse`: Evaluate and return alternate value if empty - * -[ ] `get`: Return value, throw exception if empty - * -[ ] `fold`: Apply function on optional value, return default if empty + * - {@linkcode Option#get} : Return value, throw exception if empty + * - {@linkcode Option#fold}: Apply function on optional value, return default if empty * -[ ] `map`: Apply a function on the optional value * -[ ] `flatMap`: Same as map but function must return an optional value * -[ ] `foreach`: Apply a procedure on option value @@ -74,8 +32,33 @@ function format(x: unknown): string { * -[ ] `unzip3`: Split an optional triple to three optional values * -[ ] `toList`: Unary list of optional value, otherwise the empty list * A less-idiomatic way to use Option values is via pattern matching method `match`: + * + * ```ts + * import { assertStrictEquals } from 'jsr:@std/assert' + * import { pipe } from '../internal/function.ts' + * import { Option } from './option.ts' + * + * assertStrictEquals( + * pipe( + * Option.fromNullable(undefined), + * Option.match({ + * onNone: () => 'a none', + * onSome: a => `a some containing ${a}`, + * }), + * ), + * 'a none', + * ) + * + * assertStrictEquals( + * Option.fromNullable(1).match({ + * onNone: () => 'a none', + * onSome: a => `a some containing ${a}`, + * }), + * 'a some containing 1', + * ) + * ``` */ -export abstract class Option implements Inspectable, Equals { +export abstract class Option implements Inspectable, Equals { /** * The discriminant property that identifies the type of the `Option` instance. */ @@ -93,12 +76,14 @@ export abstract class Option implements Inspectable, Equals { */ protected constructor() { if (new.target === Option) { - throw new Error('Option is not meant to be instantiated directly') + throw new Error( + "Option is not meant to be instantiated directly; instantiate instead Option's derived classes (Some, None)", + ) } } /** - * Overloads default {@linkcode Object#[Symbol#toStringTag]} getter allowing Option to return a custom string + * Overloads default {@linkcode Object.[Symbol.toStringTag]} getter allowing Option to return a custom string */ public get [Symbol.toStringTag](): string { return `Option.${this._tag}` @@ -116,12 +101,12 @@ export abstract class Option implements Inspectable, Equals { return { _id: 'Option', _tag: this._tag, - ...(this.isSome() ? { value: this.value } : {}), + ...(this.isSome() ? { value: this.get() } : {}), } } /** - * Overloads default {@linkcode Object#prototype#toString} method allowing Option to return a custom string representation of the boxed value + * Overloads default {@linkcode Object.prototype.toString} method allowing Option to return a custom string representation of the boxed value * @override */ public toString(this: Option.Type): string { @@ -133,8 +118,8 @@ export abstract class Option implements Inspectable, Equals { * * @category constructors */ - public static None(): Option.Type { - return None.getSingletonInstance() + public static None(): Option.Type { + return None.getSingletonInstance() } /** @@ -147,25 +132,116 @@ export abstract class Option implements Inspectable, Equals { return new Some(value) } + /** + * Returns a new function that takes an Option and returns the result of applying `f` to Option's value if the Option is nonempty. Otherwise, evaluates expression `ifEmpty`. + * This is equivalent to: + * ```ts + * import { Option } from './option.ts' + * + * declare const option: Option.Type + * declare const f: (a: A) => B + * declare const ifEmpty: () => B + * + * option.match({ + * onSome: (x) => f(x), + * onNone: () => ifEmpty(), + * }) + * ``` + * + * This is also equivalent to: + * ```ts no-eval no-assert + * import { Option } from './option.ts' + * + * declare const option: Option.Type + * declare const f: (a: A) => B + * declare const ifEmpty: () => B + * + * option.map(f).getOrElse(ifEmpty()) + * ``` + * + * @param ifEmpty - The expression to evaluate if empty. + * @param f - The function to apply if nonempty. + * @returns a function that takes an Option and returns the result of applying `f` to this Option's value if the Option is nonempty. Otherwise, evaluates expression `ifEmpty`. + * + * @remarks + * This is a curried function, so it can be partially applied. + * It is also known as `reduce` in other languages. + * + * @example + * ( ( ) -> B, (A) -> B ) -> Option.Type -> B + * ```ts + * import { assertStrictEquals } from 'jsr:@std/assert' + * import type * as F from '../internal/function.ts' + * + * import { Option } from './option.ts' + * + * // Define a function to execute if the option holds a value + * const increment: (x: number) => number = a => a + 1 + * // Define function to be evaluated if the Option is empty + * const ifEmpty:F.Lazy = () => 0 + * + * // Define curry function + * const incrementOrZero = Option.fold(ifEmpty, increment) + * + * const two = incrementOrZero(Option.Some(1)) + * // ^? number + * assertStrictEquals(two, 2) + * + * const zero: number = incrementOrZero(Option.None()) + * // ^? number + * assertStrictEquals(zero, 0) + * ``` + * + * @example + * ( ( ) -> B, (A) -> C ) -> Option.Type -> B | C + * ```ts + * import { assertStrictEquals } from 'jsr:@std/assert' + * import type * as F from '../internal/function.ts' + * + * import { Option } from './option.ts' + * + * const fold = Option.fold(() => null, (a: number) => a + 1) + * + * const two: number | null = fold(Option.Some(1)) + * // ^? number | null + * assertStrictEquals(two, 2) + * + * const numberOrNull: number | null = fold(Option.None()) + * // ^? number | null + * assertStrictEquals(numberOrNull, null) + * ``` + * @see {Option.match} + * @category scala3-api + */ + public static fold( + ifEmpty: F.Lazy, + f: (a: A) => C, + ): (self: Option.Type) => B | C { + return (self) => (self.isEmpty() ? ifEmpty() : f(self.get())) + } + /** * Constructs a new `Option` from a nullable type. - * @param value - An nullable value - * @returns An `Option` that is {@linkcode class None} if the value is `null` or `undefined`, otherwise a {@linkcode Some(1)} containing the value. + * @param nullableValue - An nullable nullableValue + * @returns An `Option` that is {@linkcode class None} if the nullableValue is `null` or `undefined`, otherwise a {@linkcode Some(1)} containing the nullableValue. * * @example * ```ts * import { assertStrictEquals } from 'jsr:@std/assert' * import { Option } from './option.ts' * - * assertStrictEquals(Option.fromNullable(undefined), Option.None()) - * assertStrictEquals(Option.fromNullable(null), Option.None()) - * assertStrictEquals(Option.fromNullable(1), Option.Some(1)) + * assertStrictEquals(Option.fromNullable(undefined), Option.None()) // None | Option.Some + * assertStrictEquals(Option.fromNullable(undefined as (undefined | string)), Option.None()) // None | Option.Some + * assertStrictEquals(Option.fromNullable(null), Option.None()) // None | Option.Some + * assertStrictEquals(Option.fromNullable(1), Option.Some(1)) // None | Option.Some * ``` * - * @category Constructors + * @category constructors */ - public static fromNullable(value: T): Option.Type> { - return value === undefined || value === null ? None.getSingletonInstance() : new Some(value) + public static fromNullable(nullableValue: T): Option.Type> { + return nullableValue === undefined || nullableValue === null + ? None.getSingletonInstance>() + : Option.Some(nullableValue as NonNullable) } /** @@ -183,7 +259,7 @@ export abstract class Option implements Inspectable, Equals { * ``` * @category type-guards */ - public static isNone(self: Option.Type): self is None { + public static isNone(self: Option.Type): self is None { return self instanceof None } @@ -226,6 +302,47 @@ export abstract class Option implements Inspectable, Equals { return self instanceof Some } + /** + * Curried pattern matching for `Option` instances. + * Given a pattern matching object, it will return a function that will match the `Option` instance against the pattern. + * + * @param cases - The pattern matching object + * @param cases.onNone - The lazy value to be returned if the `Option` is `None`; + * @param cases.onSome - The function to be called if the `Option` is `Some`, it will be passed the `Option`'s value and its result will be returned + * @returns A function that will match the `Option` instance against the pattern. + * + * @example + * ```ts + * import { assertStrictEquals } from 'jsr:@std/assert' + * import { Option } from './option.ts' + * import { pipe } from '../internal/function.ts' + * + * assertStrictEquals( + * pipe( + * Option.Some(1), + * Option.match({ onNone: () => 'a none', onSome: (a) => `a some containing ${a}` }) + * ), + * 'a some containing 1' + * ) + * + * assertStrictEquals( + * pipe( + * Option.None(), + * Option.match({ onNone: () => 'a none', onSome: (a) => `a some containing ${a}` }) + * ), + * 'a none' + * ) + * ``` + * @see {Option.fold} + * @category pattern matching + */ + public static match(cases: { + readonly onNone: F.Lazy + readonly onSome: (a: A) => C + }): (self: Option.Type) => B | C { + return Option.fold(cases.onNone, cases.onSome) + } + /** * Implements the {@linkcode Equals} interface, providing a way to compare two this Option instance with another unknown value that may be an Option or not. * @@ -236,6 +353,7 @@ export abstract class Option implements Inspectable, Equals { * In its unary form, it uses referential equality (employing the Object.is algorithm). This behavior can be overridden by providing a custom predicate strategy as second argument. * * @example + * Using the default referential equality strategy on primitive types: * ```ts * import { Option } from './option.ts' * import { assertStrictEquals, equal } from 'jsr:@std/assert' @@ -247,16 +365,33 @@ export abstract class Option implements Inspectable, Equals { * assertStrictEquals(some1.equals(some1), true) * assertStrictEquals(some1.equals(none), false) * assertStrictEquals(none.equals(none), true) + * ``` + * + * @example + * Using the default referential equality strategy on non-primitive types: + * ```ts + * import { Option } from './option.ts' + * import { assertStrictEquals, equal } from 'jsr:@std/assert' + * + * const some1 = Option.Some(1) + * const none = Option.None() * * // equality derive types * assertStrictEquals(some1.equals(Option.Some(1)), true) * const someRecord = Option.Some({ foo: 'bar' }) * assertStrictEquals(someRecord.equals(someRecord), true) * assertStrictEquals(someRecord.equals(Option.Some({ foo: 'bar' })), false) + * ``` + * + * @example + * Using a custom predicate strategy: + * ```ts + * import { Option } from './option.ts' + * import { assertStrictEquals, equal } from 'jsr:@std/assert' * * // equality with custom predicate strategy * assertStrictEquals( - * someRecord.equals( + * Option.Some({ foo: 'bar' }).equals( * Option.Some({ foo: 'bar' }), * equal, // a custom deep equality strategy * ), @@ -272,17 +407,84 @@ export abstract class Option implements Inspectable, Equals { return this.isSome() ? Option.isOption(that) && Option.isSome(that) && - predicateStrategy(this.value, that.value as That) + predicateStrategy(this.get(), that.get() as That) : Option.isOption(that) && Option.isNone(that) } + /** + * Returns the result of applying f to this Option's value if the Option is + * nonempty. Otherwise, evaluates expression ifEmpty. + * + * @param ifEmpty - The expression to evaluate if empty. + * @param f - The function to apply if nonempty. + * @returns The result of applying `f` to this Option's value if the Option is nonempty. Otherwise, evaluates expression `ifEmpty`. + * + * @example + * ```ts + * import { Option } from './option.ts' + * import { assertStrictEquals } from 'jsr:@std/assert' + * + * assertStrictEquals(Option.Some(1).fold(() => 0, (a) => a + 1), 2) + * assertStrictEquals(Option.Some(1).fold(() => 0, (a) => a + 1), 0) + * ``` + * @category scala3-api + * @see {Option.fold} + */ + public fold( + this: Option.Type, + ifEmpty: F.Lazy, + f: (a: NoInfer) => C, + ): B | C { + return Option.fold(ifEmpty, f)(this) + } + + /** + * Returns the option's value. + * @throws Error - if the option is empty + * @remarks The option must be nonempty. + */ + public abstract get(): A + + /** + * Returns the option's value if the option is nonempty, otherwise return the result of evaluating default. + * + * This is equivalent to the following pattern match: + * ```ts no-assert + * import { Option } from './option.ts' + * + * declare const option: Option.Type + * declare const defaultValue: () => B + * + * option.match({ + * onNone: () => defaultValue(), + * onSome: (a) => a + * }) + * ``` + * + * @param defaultValue - T the default expression. It will be evaluated if the option is empty. + */ + public getOrElse(this: Option.Type, defaultValue: F.Lazy): A | B { + return this.isEmpty() ? defaultValue() : this.get() + } + + /** + * Type guard that returns `true` if the option is None, `false` otherwise. + * + * @returns `true` if the option is None, `false` otherwise. + * @alias isNone + * @category type-guards, scala3-api + */ + isEmpty(this: Option.Type): this is None { + return this.isNone() + } + /** * Type guard that checks if the `Option` instance is a {@linkcode None}. * @returns `true` if the `Option` instance is a {@linkcode None}, `false` otherwise. * * @category type-guards */ - public isNone(this: Option.Type): this is None { + public isNone(this: Option.Type): this is None { return Option.isNone(this) } @@ -294,6 +496,52 @@ export abstract class Option implements Inspectable, Equals { public isSome(this: Option.Type): this is Some { return Option.isSome(this) } + + /** + * Pattern matches the value of the Option. + * + * @this {Some | None} - The `Option` instance to match. + * @param cases - The pattern matching object containing callbacks for different case branches: + * @param cases.onNone - The lazy value to be returned if the `Option` is `None`; + * @param cases.onSome - The function to be called if the `Option` is `Some`, it will be passed the `Option`'s value and its result will be returned + * @returns The value returned by the corresponding callback from the cases object of type `B` or `C`. + * + * @example + * ```ts + * import { assertStrictEquals } from 'jsr:@std/assert' + * import { Option } from './option.ts' + * + * assertStrictEquals( + * Option.fromNullable(1) + * .match({ + * onNone: () => 'a none', + * onSome: (a) => `a some containing ${a}` + * }), + * 'a some containing 1' + * ) + * + * assertStrictEquals( + * Option.fromNullable(null) + * .match({ + * onNone: () => 'a none', + * onSome: (a) => `a some containing ${a}` + * }), + * 'a none' + * ) + * ``` + * @see {Option.fold} + * @see {Option.match} + * @category pattern-matching + */ + public match( + this: Option.Type, + cases: { + readonly onNone: F.Lazy + readonly onSome: (a: A) => C + }, + ): B | C { + return Option.match(cases)(this) + } } /** @@ -315,27 +563,52 @@ export declare namespace Option { * * @category type-level */ - export type Type = None | Some + export type Type = None | Some + + /** + * Type utility to extract the type of the value from an Option. + * + * @example + * ```ts + * import { Option } from './option.ts' + * + * const aNumber: number = 42 + * const someOfNumber: Option.Type = Option.Some(aNumber) + * const test1: Option.Value = aNumber // ✅ no ts error! + * // ^? number + * + * // @ts-expect-error Type 'string' is not assignable to type 'number'. + * const test2: Option.Value = "42" // 💥ts error! + * ``` + */ + export type Value> = [T] extends [Option.Type] ? _A + : never } /** * Case class representing the absence of a value. * @class None * @extends Option - * @public + * @internal */ -class None extends Option { - static #instance: undefined | None = undefined - +export class None extends Option { + static #instance: undefined | None = undefined public readonly _tag = 'None' as const + /** + * @inheritDoc + */ + public get(): never { + throw new Error('None.get') + } + /** * Creates a new immutable `None` instance. * * @returns An instance of `None` * @remaks * We don't need multiple instances of `None` in memory, therefore we can save memory by using a singleton pattern. - * Do not call this constructor directly. Instead, use {@linkcode None#getSingletonInstance}. + * Do not call this constructor directly. Instead, use {@linkcode None.getSingletonInstance}. * @hideconstructor */ private constructor() { @@ -347,15 +620,15 @@ class None extends Option { * Returns the singleton instance of `None`. * * @returns The singleton instance of `None`. - * @internal - use instead {@linkcode Option#None} + * @internal - use instead {@linkcode Option.None} * * @category constructors */ - public static getSingletonInstance(): None { + public static getSingletonInstance(): None { if (!None.#instance) { - None.#instance = new None() + None.#instance = new None() } - return None.#instance + return None.#instance as None } } @@ -363,11 +636,18 @@ class None extends Option { * Case class representing the presence of a value. * @class Some * @extends Option - * @public + * @internal */ -class Some extends Option { +export class Some extends Option { public readonly _tag = 'Some' as const + /** + * @inheritDoc + */ + public get(): A { + return this.#value + } + readonly #value: A /** @@ -375,7 +655,7 @@ class Some extends Option { * * @param value - The `value` to wrap. * @returns An instance of `Some` - * @remarks Do not call this constructor directly. Instead, use the static {@linkcode Option#Some} + * @remarks Do not call this constructor directly. Instead, use the static {@linkcode Option.Some} * @hideconstructor */ public constructor(value: A) { @@ -383,8 +663,4 @@ class Some extends Option { this.#value = value Object.freeze(this) } - - public get value(): A { - return this.#value - } }