-
Notifications
You must be signed in to change notification settings - Fork 68
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
Early return / try! / ? operator #43
Comments
I'm wondering the same thing. When there's no async, const read = (path: string, key: string) =>
validatePath(path)
.andThen((validPath) => readBytes(validPath))
.andThen((fileContents) => decrypt(fileContents, key)); The problem is that Promise and this library implement two concurrent monad models. One way to solve that would be to integrate Promises in this library. E.g. new signatures like const read = (path: string, key: string) =>
(await validatePath(path)
.andThen((validPath) => readBytes(validPath)))
.andThen((fileContents) => decrypt(fileContents, key)); Another way to do that is having a TypeScript plugin than can transpile a custom extra syntax. This would be an actual early return! But I'm not aware of such project, and this could mess with the other dev tools. |
Follow-up thinking. My last code could be written this way, thus avoiding the nested await: const read = (path: string, key: string) =>
validatePath(path)
.andThen((validPath) => readBytes(validPath))
.then((result) => result.andThen((fileContents) => decrypt(fileContents, key))); I'll actually try this construct on a project right away. Maybe a TypeScript plugin could detect and optimize this pattern, but maybe the V8 JITter is already doing that. |
There's a couple other options: wrap everything in a helper to catch errors, add another common API and deal with the results. Wrap everything in a helper to catch errorsIf there was a helper like: function catchErr<Value, E>(
callback: () => Result<Value, E> | Promise<Result<Value, E>>
): Result<Value, E> | Promise<Result<Value, E>> {
try {
return callback();
} catch (error) {
if (Result.isResult(error)) {
return error;
}
throw error;
}
} And the ability to throw an actual interface ErrImpl<E> {
questionMark(): never;
}
interface OkImpl<T> {
questionMark(): T;
} Then, you could do something like: const readWithCatchErr = (path: string, key: string): Promise<Result<Something, ReadError>> => {
return catchErr(async () => {
const validPath: string = validatePath(path).questionMark();
const fileContents: Buffer = (await readBytes(validPath)).questionMark();
const decrypted: Something = (await decrypt(fileContents, key)).questionMark();
return new Ok(decrypted);
});
}; Add another common API and deal with the resultsThis is basically the idea suggested above:
But with a different name, so the semantics of There's a common pattern in some purely functional languages called interface ErrImpl<E> {
traverse<Value, Error = E>(_callback: unknown): Promise<Err<E | Error>> {
return Promise.resolve(this);
};
}
interface OkImpl<T> {
traverse<Value, Error>(
callback: (value: T) => Promise<Result<Value, Error>>
): Promise<Result<Value, Error>> {
return callback(this.val);
}
} Then you could do something like this: const readWithTraverse = async (
path: string,
key: string
): Promise<Result<Something, ReadError>> => {
const validPath: Result<string, ReadError> = validatePath(path);
const fileContents: Result<Buffer, ReadError> = await validPath.traverse(readBytes);
const decrypted: Result<Something, ReadError> = await fileContents.traverse((contents: Buffer) =>
decrypt(contents, key)
);
return decrypted;
}; The naming probably would be bikeshed, and surely there's an edge case or two in the first idea. But these are some ways you could achieve the flattening you get with Rust without too much noise or going off the separate language idea. Here's a playground of both ideas. |
… Too much boilerplate (vultix/ts-results#43)
Here's a library for macros, ts-macros, but it’s still in early development. |
This library seems quite nice, but it has a main downside compared to Rust code: The lack of early-return on errors.
Take this as an example:
This is quite nice and it shows exactly what can go wrong, in a typesafe way.
However, if I want to refactor this and break it up into three functions:
That's... not better 😕
Yes, I could use
.map
to chain calculations, but this is a simplified example. Real-world code may be much more complex, and then .map chains get more tedious. I made this experience with early async support in Rust: Initially you had to build future chains, and it was a big pain. Then async/await came, and all of a sudden you could write async code like sync code, without combinators and chaining.The difference between TS and Rust is that Rust provides a
?
operator for early-return of errors. Before that, it had atry!
macro that did the same.Is there any mechanism in NodeJS to emulate this? I'm not aware of any macro-like functionality at the moment, so I don't think this is possible 😕
The text was updated successfully, but these errors were encountered: