nalloc
JavaScript速度下的rust般的安全。
类型安全的 Option 和 Result 为TypeScript提供近乎零的分配,以获得极致的性能。
为什么nalloc ?
大多数选项/结果库将每个值包装在一个对象中。nalloc没有。
Other libraries: Some(42) --> { _tag: 'Some', value: 42 } // allocates
nalloc: Some(42) --> 42 // zero allocation
Other libraries: Ok(data) --> { _tag: 'Ok', value: data } // allocates
nalloc: Ok(data) --> data // zero allocation
- Rust-like类型 -
Option<T>和Result<T, E>具有完整的模式匹配和组合子 - 接近于零的分配 -
Some和Ok是价值观本身。只有Err分配。 - 极端的表现 -比快乐路径上的替代方案快2倍
基准
| 操作 | nalloc | neverthrow | ts-results | oxide.ts |
|---|---|---|---|---|
ok() | 91行动/ s | 75行动/ s | 56 m行动/ s | 40米运维/ s |
err() | 55米运维/ s | 52米运维/ s | 0.1行动/ s | 47米运维/ s |
some() | 85行动/ s | - | 40米运维/ s | 50米运维/ s |
Result.map | 74行动/ s | 53米运维/ s | 47米运维/ s | 64行动/ s |
Option.map | 71行动/ s | - | 52米运维/ s | 41米运维/ s |
运行 pnpm bench 复制。
安装
npm install nalloc
快速启动
import { Option, Result, ok, err, none, pipe, gen } from 'nalloc';
// Option: the value IS the Option
const port = Option.fromNullable(process.env.PORT);
const portNum = Option.unwrapOr(port, '3000');
// Result: Ok is the value itself, Err wraps the error
const config = Result.tryCatch(
() => JSON.parse(raw),
() => 'invalid json',
);
const data = Result.match(
config,
(cfg) => cfg,
(error) => ({ fallback: true }),
);
// gen: Rust-like ? operator with type-safe errors
const result = gen(function* ($) {
const a = yield* $(parseNumber('10'));
const b = yield* $(parseNumber('5'));
return a + b;
}); // Result<number, ParseError>
// pipe: left-to-right composition
const userId = pipe(
Result.tryCatch(() => JSON.parse(raw)),
(r) => Result.map(r, (data) => data.userId),
(r) => Result.unwrapOr(r, 0),
);
它是如何工作的
| 类型 | 表示 | 分配 |
|---|---|---|
Option<T> | T | null | undefined | 零 |
Some<T> | 价值本身 | 零 |
None | null 或 undefined | 零 |
Ok<T> | 价值本身(品牌) | 零 |
Err<E> | 最小的包装 { error: E } | 一个对象 |
这意味着快乐路径上的GC压力为零——您的成功值保持为普通值。
来自其他图书馆吗?
从neverthrow
// neverthrow
import { ok, err, Result } from 'neverthrow';
const result: Result<number, string> = ok(42);
result.map((x) => x * 2);
// nalloc - same concepts, faster execution
import { Result, ok, err } from 'nalloc';
const result = ok(42); // zero allocation
Result.map(result, (x) => x * 2); // function-based API
从fp-ts
// fp-ts
import { pipe } from 'fp-ts/function';
import * as O from 'fp-ts/Option';
pipe(
O.some(42),
O.map((x) => x * 2),
);
// nalloc - simpler, faster
import { Option, pipe } from 'nalloc';
pipe(42, (v) => Option.map(v, (x) => x * 2)); // built-in pipe, value IS the Option
从oxide.ts
// oxide.ts
import { Some, None, Ok, Err } from 'oxide.ts';
const opt = Some(42);
opt.map((x) => x * 2);
const result = Ok(42);
result.mapErr((e) => new Error(e));
// nalloc - no wrapper objects on the happy path
import { Option, Result, ok } from 'nalloc';
Option.map(42, (x) => x * 2); // 42 is the Option itself
const result = ok(42); // zero allocation
Result.mapErr(result, (e) => new Error(e));
从ts-results
// ts-results
import { Ok, Err, Some, None } from 'ts-results';
const result = new Ok(42);
result.map((x) => x * 2);
const opt = Some(42);
opt.unwrapOr(0);
// nalloc - same safety, zero allocations
import { Option, Result, ok } from 'nalloc';
const result = ok(42); // no wrapper
Result.map(result, (x) => x * 2);
Option.unwrapOr(42, 0); // value IS the Option
从生锈
这个API反映了Rust的API Option 和 Result:
import { Option, Result, ok, err, none, gen } from 'nalloc';
// Rust: Some(42).map(|x| x * 2)
Option.map(42, (x) => x * 2);
// Rust: Ok(42).and_then(|x| if x > 0 { Ok(x) } else { Err("negative") })
Result.andThen(ok(42), (x) => (x > 0 ? ok(x) : err('negative')));
// Rust: result.unwrap_or(0)
Result.unwrapOr(result, 0);
// Rust: let value = get_value()?
const value = gen(function* ($) {
const a = yield* $(getValue());
return a + 1;
});
选项
一个 Option<T> 是 T | null | undefined。值本身就是Option。
import { Option, none } from 'nalloc';
// Create from nullable
const maybePort = Option.fromNullable(process.env.PORT);
// Transform
const doubled = Option.map(maybePort, (p) => parseInt(p) * 2);
const validated = Option.filter(doubled, (n) => n > 0);
// Extract
const port = Option.unwrapOr(maybePort, '3000');
// Pattern match
const label = Option.match(
maybePort,
(value) => `Port: ${value}`,
() => 'Port: default',
);
// Assert and narrow
Option.assertSome(maybePort, 'PORT is required');
// Filter collections
const activeIds = Option.filterMap(users, (user) => (user.isActive ? user.id : none));
// Async
const maybeUser = await Option.fromPromise(fetchUserById(id));
结果
一个 Result<T, E> 要么是 Ok<T> (价值本身)或 Err<E> (包装)。
import { Result, Schema, ok, err, gen, genAsync, pipe } from 'nalloc';
// Wrap throwing functions
const parsed = Result.tryCatch(
() => JSON.parse(raw),
(e) => 'invalid json',
);
// gen: Rust-like ? operator with preserved error types
const result = gen(function* ($) {
const config = yield* $(parseConfig(raw));
const db = yield* $(connectDb(config.url));
return db.query('SELECT 1');
}); // Result<QueryResult, ConfigError | DbError>
// Async gen
const data = await genAsync(async function* ($) {
const res = yield* $(await Result.fromPromise(fetchUser(id)));
const posts = yield* $(await Result.fromPromise(fetchPosts(res.id)));
return { user: res, posts };
}); // Promise<Result<{user, posts}, unknown>>
// wrap / toThrowable: ecosystem boundaries
const safeParse = Result.wrap(JSON.parse); // throwing -> Result
safeParse('{"a":1}'); // Ok({a: 1})
const throwingFind = Result.toThrowable(findUser); // Result -> throwing
throwingFind('123'); // returns User or throws
// Standard Schema validation (Zod, Valibot, ArkType, etc.)
const validated = Schema.fromSchema(userSchema, input);
// Result<User, readonly SchemaIssue[]>
// wrapSchema: bind a schema once, reuse the validator
const parseUser = Schema.wrapSchema(userSchema);
// Transform with pipe
const userId = pipe(
parsed,
(r) => Result.map(r, (data) => data.userId),
(r) => Result.flatMap(r, (id) => (id > 0 ? ok(id) : err('invalid id'))),
);
// Pattern match
const user = Result.match(
parsed,
(data) => data.user,
(error) => null,
);
// Assert and narrow
Result.assertOk(loaded, 'Config required');
// Collections
const combined = Result.all([ok(1), ok(2), ok(3)]); // Ok([1, 2, 3])
const first = Result.any([err('a'), ok(42), err('b')]); // Ok(42)
const [oks, errs] = Result.partition(results);
Iter
用于处理易出错迭代和结果流的迭代器实用程序。
import { Iter } from 'nalloc';
// Safe iteration: wraps throws as Err, values as Ok
for (const result of Iter.safeIter(riskyIterator)) {
if (isOk(result)) handleValue(result);
else handleError(result.error);
}
// mapWhile: yields mapped values while fn returns Some
const taken = [...Iter.mapWhile([1, 2, 3, 4, 5], (n) => (n < 4 ? n * 10 : null))]; // [10, 20, 30]
// tryCollect: collect Results into Result<T[], E>
const collected = Iter.tryCollect(results); // Ok([1, 2, 3]) or first Err
// tryFold: fold with early exit on Err
const sum = Iter.tryFold([1, 2, 3], 0, (acc, n) => ok(acc + n));
// tryForEach: iterate with early exit on Err
Iter.tryForEach(items, (item) => processItem(item));
非空的
在类型级别证明数组至少有一个元素。运行时
Value是一个普通数组——没有包装器,没有类,没有克隆。 fromArray 返回
非空时的原始数组值。
import { NonEmpty, Option } from 'nalloc';
NonEmpty.isNonEmpty([]); // false
NonEmpty.isNonEmpty([1]); // true, narrows to ReadonlyNonEmptyArray<number>
const values: readonly number[] = loadValues();
if (NonEmpty.isNonEmpty(values)) {
const first: number = values[0]; // typed, no undefined
}
// fromArray returns None for [] and Some(values) otherwise
const opt = NonEmpty.fromArray(values); // Option<ReadonlyNonEmptyArray<number>>
两个Result组合子现在在其错误类型中携带非空保证:
Result.collectAll(results)返回Result<T[], NonEmptyArray<E>>-如果 结果是Err,则保证错误数组至少包含一个条目。Result.any(results)返回Result<Widen<T>, NonEmptyArray<WidenNever<E>>>出于同样的原因:any只有当每个输入都失败时才会失败。
API参考
选项
| 函数 | 描述 |
|---|---|
fromNullable(value) | 将nullable转换为Option |
fromPromise(promise) | 对选项的承诺(拒绝=无) |
map(opt, fn) | 变换某个值 |
flatMap(opt, fn) | 链式选项返回函数 |
andThen(opt, fn) | flatMap别名 |
tap(opt, fn) | 副作用有的,返璞归真 |
tapNone(opt, fn) | 无副作用,返回原来的 |
filter(opt, predicate) | 如果谓词通过,则保留一些 |
match(opt, onSome, onNone) | 模式匹配 |
unwrap(opt) | 取出或丢弃 |
unwrapOr(opt, default) | 提取或默认 |
unwrapOrElse(opt, fn) | 提取或计算默认值 |
unwrapOrReturn(opt, fn) | 提取或返回计算值 |
expect(opt, msg) | 提取或抛出消息 |
assertSome(opt, msg?) | 断言并缩小到一些 |
isSome(opt) / isNone(opt) | 类型警卫 |
isSomeAnd(opt, pred) | 一些和谓词传递 |
isNoneOr(opt, pred) | 无或谓词通过 |
or(opt, other) | 先返回一些 |
orElse(opt, fn) | 返回一些或计算回退 |
and(opt, other) | 如果first是Some,则返回second |
xor(opt, other) | 如果正好有一个是 |
zip(opt, other) | 将两个选项组合成元组 |
unzip(opt) | 将tuple Option拆分为两个 |
flatten(opt) | 平坦嵌套选项 |
contains(opt, value) | 检查Some是否包含value |
mapOr(opt, default, fn) | 映射或返回默认值 |
mapOrElse(opt, defaultFn, fn) | 映射或计算默认值 |
toArray(opt) | 有的去[value],没有去[] |
toNullable(opt) | Some为value, None为null |
toUndefined(opt) | 有些值为value,没有值为undefined |
okOr(opt, error) | 结果选项 |
okOrElse(opt, fn) | 选项以计算错误的结果 |
ofOk(result) | 有些人对,没有人错 |
ofErr(result) | 有些人犯错,没有人好 |
filterMap(items, fn) | 映射和过滤器在一次通过 |
findMap(items, fn) | 先从映射中找到一些 |
结果
| 函数 | 描述 |
|---|---|
tryCatch(fn, onError?) | Wrap抛出函数 |
tryCatchMaybePromise(fn, onError?) | 包装同步或异步,保持同步 |
of(fn) | tryCatch的别名(没有错误映射器) |
wrap(fn, onError?) | 一次性封装抛出函数,重用 |
toThrowable(fn) | wrap的反转:结果返回到抛出 |
gen(fn) | 带有输入错误的生成器do-notation |
genAsync(fn) | 异步生成器表示法 |
safeTry(fn) | 带展开的命令式错误处理 |
safeTryAsync(fn) | Async版本的safeTry |
unwrap(result) | 提取Ok或抛出错误值 |
unwrapErr(result) | 提取Err或throw |
unwrapOr(result, default) | 提取Ok或默认值 |
unwrapOrElse(result, fn) | 提取Ok或从错误中计算 |
unwrapOrReturn(result, fn) | 提取Ok或返回计算值 |
expect(result, msg) | 提取Ok或抛出消息 |
expectErr(result, msg) | 提取带有消息的Err或throw |
map(result, fn) | 变换Ok值 |
mapErr(result, fn) | 转换Err值 |
bimap(result, okFn, errFn) | 转换Ok和Err |
flatMap(result, fn) | 链式返回结果函数 |
andThen(result, fn) | flatMap别名 |
tap(result, fn) | 副作用在好的,返回原来的 |
tapErr(result, fn) | 副作用Err,返回原来的 |
match(result, onOk, onErr) | 模式匹配 |
assertOk(result, msg?) | 断言并缩小为Ok |
assertErr(result, msg?) | 断言并缩小到Err |
isOk(result) / isErr(result) | 类型警卫 |
isOkAnd(result, pred) | 好的,谓词通过了 |
isErrAnd(result, pred) | Err和谓词传递 |
isSomeErr(result) | 非空错误 |
and(result, other) | 如果第一个正确,则返回第二个 |
or(result, other) | 先返回Ok |
orElse(result, fn) | 好的,或者计算错误的回退 |
zip(a, b) | 把两个Ok组合成tuple |
zipWith(a, b, fn) | 结合两个Ok和函数 |
flatten(result) | 平坦嵌套结果 |
transpose(result) | 结果 |
toOption(result) | 有些人对,没有人错 |
toErrorOption(result) | 有些人犯错,没有人好 |
mapOr(result, default, fn) | 映射Ok或返回默认值 |
mapOrElse(result, defaultFn, fn) | 映射Ok或计算默认值 |
all(results) | 收集全部Ok或第一个Err |
any(results) | 首先Ok or all errors |
collect(results) | 收集Ok值或第一个Err |
collectAll(results) | 要么一切正常,要么一切错误 |
partition(results) | 分成[ok, errors] |
partitionAsync(promises) | 异步分区 |
filterOk(results) | 提取所有Ok值 |
filterErr(results) | 提取所有Err值 |
settleMaybePromise(values) | 将sync/async值设置为Results |
partitionMaybePromise(values) | 分区sync/async结果 |
模式
标准模式v1验证。可摇树:导入 Schema 的命名空间 nalloc,或直接从 nalloc/schema。
| 函数 | 描述 |
|---|---|
fromSchema(schema, value) | 通过标准模式v1进行验证,返回结果 |
wrapSchema(schema) | 绑定一次模式,返回一个可重用的验证器 |
HTTP
可选 nalloc/http 子路径。可以在任何运行时中使用 fetch (浏览器、Node 18+、Bun、Deno、edge worker);被保留在主入口之外,因此内核不受环境类型的全局变量的影响。
本地的 fetch 成功解析4xx/5xx -仅传输失败拒绝。因此,通用帮助程序单独让HTTP错误沿着Ok路径流动,看起来像成功:
const r = await Result.fromPromise(fetch(url)); // Ok(Response { status: 404 }) - not an Err!
fromFetch Ok表示请求连接并返回2xx,并且每个失败模式都落在错误通道中,输入:
import { fromFetch } from 'nalloc/http';
const res = await fromFetch(url);
// Result<Response, Response | TypeError | DOMException>
// Ok(Response) -> connected and 2xx
// Err(Response) -> reached the server, non-2xx (read status/body from it)
// Err(TypeError) -> network/CORS failure
// Err(DOMException) -> aborted or timed out
已经拿着了。 Response (自定义获取包装,中间件,不同的HTTP客户端)? fromResponse 状态检查是单独的,和组合的吗 fromPromise 如果您需要分别执行这两个步骤:
import { fromResponse } from 'nalloc/http';
const checked = fromResponse(response); // Ok(response) if response.ok, else Err(response)
| 函数 | 描述 |
|---|---|
fromFetch(input, init?) | 对于2xx运行fetch: Ok(响应),对于非2xx或传输失败运行Err |
fromResponse(response) | Ok(响应)如果响应。好的,else Err(response) |
Iter
| 函数 | 描述 |
|---|---|
safeIter(source) | 将iterable: values包装为Ok,抛出为Err |
mapWhile(source, fn) | 生成映射值,而fn返回Some |
tryCollect(source) | 收集结果到Result<T[], E> |
tryFold(source, init, fn) | 在Err时提前退出 |
tryForEach(source, fn) | 在Err时提前退出迭代 |
公用事业公司
| 函数 | 描述 |
|---|---|
pipe(value, ...fns) | 从左到右通过函数的线程值 |
ESLint插件
nalloc提供了一个可选的ESLint插件(flat config) nalloc/eslint。它只在开发时运行
永远不会进入运行时包。typescript-eslint工具链是一个可选的peer -安装它
除了ESLint:
npm install -D eslint @typescript-eslint/parser @typescript-eslint/utils typescript
// eslint.config.js
import nalloc from 'nalloc/eslint';
import tsParser from '@typescript-eslint/parser';
export default [
{
files: ['**/*.ts'],
languageOptions: {
parser: tsParser,
parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname },
},
plugins: { nalloc },
rules: {
'nalloc/must-use': 'error',
'nalloc/no-unwrap': 'error',
},
},
{ files: ['**/*.test.ts'], rules: { 'nalloc/no-unwrap': 'off' } },
];
| 规则 | 它标记了什么 | 类型信息 |
|---|---|---|
nalloc/must-use | 一个 Result/Option 值用作裸语句并被丢弃 | 要求(parserOptions.projectService) |
nalloc/no-unwrap | unwrap / unwrapErr / expect / expectErr,这意味着失败 | 不是必需的 |
nalloc.configs.recommended 打开两个规则;它不配置解析器,所以是类型敏感的
设置为 must-use 仍然是你的配置的责任。如果没有类型info, must-use 报告
标准typescript-eslint parserServices错误 no-unwrap 保持工作。
Codemod:从neverthrow迁移过来
Nalloc绑定了一个最努力的代码模块,它将neverthrow代码重写为Nalloc。它需要
可选 oxc-parser Peer(仅在开发时,不在运行时包中):
npm install -D oxc-parser
npx nalloc-codemod src --report migration-report.md # add --dry to preview
转换内容:neverthrow导入(保留别名);方法调用到函数调用
(r.map(f) -> Result.map(r, f), .andThen -> Result.flatMap, .mapErr, .orElse, .match,
.unwrapOr, .isOk/.isErr, .andTee/.orTee -> tap/tapErr, _unsafeUnwrap -> Result.unwrap);
两种或两种以上方法的链折叠成 pipe(...); fromThrowable -> Result.wrap;
combine -> Result.all; combineWithAllErrors -> Result.collectAll;directly-awaited
fromPromise -> Result.fromPromise; Result<T, E> 类型注释-> ResultType<T, E>。
编辑是文本拼接,因此未修改的代码保持字节对字节的格式化。
它拒绝和报告的内容: ResultAsync 以及所有产生1的东西
(okAsync, errAsync,链接 fromPromise, asyncMap,……)- nalloc习语是 genAsync,
这是手工重写; safeTry 发电机(使用 gen); 名称空间进口
(import * as nt). 它们保留了一个剩余的neverthrow导入,因此文件仍然在迁移过程中编译。
两个安全属性要知道。转换是基于来源的:方法名称有歧义,例如 .map
只有当接收者可以证明持有一个结果(构造自 ok/err/combine/
一个 fromThrowable-包装函数,用neverthrow类型注释,或用
永远只抛出方法)-一个裸 .map 在一个未知的接收器上,假设它是一个数组并离开
独自一人。并在一次运行中迁移整个项目:转换的调用站点假设它们的值为
Nalloc值,它只在生成它们的构造函数也被转换后才有效。
比较
| 特征 | nalloc | neverthrow | fp-ts | oxide.ts | ts-results |
|---|---|---|---|---|---|
| Zero-alloc选项 | 是 | 否 | 否 | 否 | 否 |
| Zero-alloc好 | 是 | 否 | 否 | 否 | 否 |
| 包大小 | 小 | 小 | 大 | 小 | 小 |
| 学习曲线 | 低 | 低 | 高 | 低 | 低 |
| 异步支持 | 是 | 是 | 是 | 有限的 | 否 |
| Tree-shakeable | 是 | 是 | 是 | 是 | 是 |
| 迭代器实用工具 | 是 | 否 | 是 | 否 | 否 |
| 创(?操作符) | 是 | 是 | 否 | 否 | 否 |
| 管 | 是 | 否 | 是 | 否 | 否 |
| 模式验证 | 是 | 否 | 否 | 否 | 否 |
| 生态系统互操作 | 是 | 否 | 否 | 否 | 否 |
选择
寻找TypeScript的结果/选项库?下面是nalloc的对比:
- neverthrow -流行的方法链API。为每个Ok和Err分配包装器。Nalloc完全避免了快乐路径上的分配。
- Fp-ts /效应 -全功能编程生态系统,可选可选。强大但沉重和陡峭的学习曲线。nalloc用一个更简单的API关注结果/选项。
- oxide.ts - rust灵感与基于类的包装。每个Some/Ok分配一个对象。nalloc将Some/Ok表示为原始值。
- Ts-results / Ts-results -results -直接Rust端口与类实例。类似于nalloc的API表面,但在每个构造上分配。
- 真理神话 -结果和可能与功能API。包装所有值。Nalloc是类似的零分配设计理念。
Nalloc是为分配压力很重要的代码库而设计的——高吞吐量服务器、热循环和性能敏感路径——同时保持相同的安全保证。
许可证
麻省理工学院
贡献
贡献欢迎!打开issue或PR获取想法、bug修复或文档。