diff --git a/deno.json b/deno.json index c5ee7c105f29..3b2a41c5215c 100644 --- a/deno.json +++ b/deno.json @@ -68,6 +68,7 @@ "./fmt", "./front_matter", "./fs", + "./functions", "./html", "./http", "./ini", diff --git a/functions/deno.json b/functions/deno.json new file mode 100644 index 000000000000..7cb527aba535 --- /dev/null +++ b/functions/deno.json @@ -0,0 +1,8 @@ +{ + "name": "@std/functions", + "version": "0.1.0", + "exports": { + ".": "./mod.ts", + "./pipe": "./pipe.ts" + } +} diff --git a/functions/mod.ts b/functions/mod.ts new file mode 100644 index 000000000000..a2f74fe008e2 --- /dev/null +++ b/functions/mod.ts @@ -0,0 +1,23 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +// This module is browser compatible. + +/** + * Utilities for working with functions. + * + * ```ts + * import { pipe } from "@std/functions"; + * import { assertEquals } from "@std/assert"; + * + * const myPipe = pipe( + * Math.abs, + * Math.sqrt, + * Math.floor, + * (num: number) => `result: ${num}`, + * ); + * assertEquals(myPipe(-2), "result: 1"); + * ``` + * + * @module + */ +export * from "./pipe.ts"; diff --git a/functions/pipe.ts b/functions/pipe.ts new file mode 100644 index 000000000000..bbf23c0d27d8 --- /dev/null +++ b/functions/pipe.ts @@ -0,0 +1,140 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +// deno-lint-ignore-file no-explicit-any +type AnyFunc = (...arg: any) => any; + +type LastFnReturnType, Else = never> = F extends [ + ...any[], + (...arg: any) => infer R, +] ? R + : Else; + +// inspired by https://dev.to/ecyrbe/how-to-use-advanced-typescript-to-define-a-pipe-function-381h +type PipeArgs = F extends [ + (...args: infer A) => infer B, +] ? [...Acc, (...args: A) => B] + : F extends [(...args: infer A) => any, ...infer Tail] + ? Tail extends [(arg: infer B) => any, ...any[]] + ? PipeArgs B]> + : Acc + : Acc; + +/** + * Composes functions from left to right, the output of each function is the input for the next. + * + * @example Usage + * ```ts + * import { assertEquals } from "@std/assert"; + * import { pipe } from "@std/functions"; + * + * const myPipe = pipe( + * Math.abs, + * Math.sqrt, + * Math.floor, + * (num: number) => `result: ${num}`, + * ); + * assertEquals(myPipe(-2), "result: 1"); + * ``` + * + * @param input The functions to be composed + * @returns A function composed of the input functions, from left to right + */ +export function pipe(): (arg: T) => T; +export function pipe< + Fn1 extends AnyFunc, + Fn2 extends (arg: ReturnType) => any, +>( + firstFunction: Fn1, + function2: Fn2, +): (...args: Parameters) => ReturnType; + +export function pipe< + Fn1 extends AnyFunc, + Fn2 extends (arg: ReturnType) => any, + Fn3 extends (arg: ReturnType) => any, +>( + firstFunction: Fn1, + function2: Fn2, + function3: Fn3, +): (...args: Parameters) => ReturnType; + +export function pipe< + Fn1 extends AnyFunc, + Fn2 extends (arg: ReturnType) => any, + Fn3 extends (arg: ReturnType) => any, + Fn4 extends (arg: ReturnType) => any, +>( + firstFunction: Fn1, + function2: Fn2, + function3: Fn3, + function4: Fn4, +): (...args: Parameters) => ReturnType; + +export function pipe< + Fn1 extends AnyFunc, + Fn2 extends (arg: ReturnType) => any, + Fn3 extends (arg: ReturnType) => any, + Fn4 extends (arg: ReturnType) => any, + Fn5 extends (arg: ReturnType) => any, +>( + firstFunction: Fn1, + function2: Fn2, + function3: Fn3, + function4: Fn4, + function5: Fn5, +): (...args: Parameters) => ReturnType; + +export function pipe< + Fn1 extends AnyFunc, + Fn2 extends (arg: ReturnType) => any, + Fn3 extends (arg: ReturnType) => any, + Fn4 extends (arg: ReturnType) => any, + Fn5 extends (arg: ReturnType) => any, + Fn6 extends (arg: ReturnType) => any, +>( + firstFunction: Fn1, + function2: Fn2, + function3: Fn3, + function4: Fn4, + function5: Fn5, + function6: Fn6, +): (...args: Parameters) => ReturnType; + +export function pipe< + Fn1 extends AnyFunc, + Fn2 extends (arg: ReturnType) => any, + Fn3 extends (arg: ReturnType) => any, + Fn4 extends (arg: ReturnType) => any, + Fn5 extends (arg: ReturnType) => any, + Fn6 extends (arg: ReturnType) => any, + Fn7 extends (arg: ReturnType) => any, +>( + firstFunction: Fn1, + function2: Fn2, + function3: Fn3, + function4: Fn4, + function5: Fn5, + function6: Fn6, + function7: Fn7, +): (...args: Parameters) => ReturnType; + +export function pipe( + firstFunction: FirstFn, + ...fns: PipeArgs extends F ? F : PipeArgs +): (arg: Parameters[0]) => LastFnReturnType>; + +export function pipe( + firstFunction?: FirstFn, + ...fns: PipeArgs extends F ? F : PipeArgs +): any { + if (!firstFunction) { + return (arg: T) => arg; + } + + return (...arg: Parameters) => { + return (fns as AnyFunc[]).reduce( + (acc, fn) => fn(acc), + firstFunction(...arg), + ); + }; +} diff --git a/functions/pipe_test.ts b/functions/pipe_test.ts new file mode 100644 index 000000000000..12088433f8f3 --- /dev/null +++ b/functions/pipe_test.ts @@ -0,0 +1,40 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +import { assertEquals, assertThrows } from "@std/assert"; +import { pipe } from "./pipe.ts"; + +Deno.test("pipe() handles mixed types", () => { + const inputPipe = pipe( + Math.abs, + (num) => `result: ${num}`, + ); + assertEquals(inputPipe(-2), "result: 2"); +}); +Deno.test("pipe() handles first function with two arguments", () => { + function add(a: number, b: number): number { + return a + b; + } + const inputPipe = pipe( + add, + (num) => `result: ${num}`, + ); + assertEquals(inputPipe(3, 2), "result: 5"); +}); + +Deno.test("en empty pipe is the identity function", () => { + const inputPipe = pipe(); + assertEquals(inputPipe("hello"), "hello"); +}); + +Deno.test("pipe() throws an exceptions when a function throws an exception", () => { + const inputPipe = pipe( + Math.abs, + Math.sqrt, + Math.floor, + (num: number) => { + throw new Error("This is an error for " + num); + }, + (num: number) => `result: ${num}`, + ); + assertThrows(() => inputPipe(-2)); +}); diff --git a/import_map.json b/import_map.json index 4e2bc2352727..d781630ec9d2 100644 --- a/import_map.json +++ b/import_map.json @@ -22,6 +22,7 @@ "@std/expect": "jsr:@std/expect@^1.0.12", "@std/fmt": "jsr:@std/fmt@^1.0.4", "@std/front-matter": "jsr:@std/front-matter@^1.0.5", + "@std/functions": "jsr:@std/functions@^0.1.0", "@std/fs": "jsr:@std/fs@^1.0.10", "@std/html": "jsr:@std/html@^1.0.3", "@std/http": "jsr:@std/http@^1.0.12",