-
Notifications
You must be signed in to change notification settings - Fork 88
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
Emulation of Rust's ? operator #448
Conversation
nice |
const errVal = "err" | ||
const okValues = Array<string>() | ||
|
||
const result = safeTry(function*() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So basically I can do something like
const caller = ():Result<string,MyError> => {
// foobar: Result<string,MyError>
const res = yield* foobar().safeUnwrap()
return res + "hello"
)
}
Any contributors reviewing this? |
Sorry, life recently has made it tough to be as attentive as I used to be. I'll see if I can have a look this coming weekend. |
Bump |
Any updates? |
Hello! |
Would anyone here be willing to contribute to the docs / readme? A few examples would be much appreciated for folks who aren't so familiar with generators as well (myself included). |
Extremely basic docs have been added in this commit: 1fd6a05 |
#448 (comment) |
@tsuburin I know I'm a bit late to the party, but thanks a lot for this change, it's a great improvement!. I just have one small question, though: is there a specific reason why In addition, it might also make sense to allow passing arguments to the function passed to Example that shows this in combination: /* --- signatures for safeTry --- */
declare function safeTry<T, E, Args extends unknown[]>(
body: (...args: Args[]) => Generator<Err<never, E>, T>,
...args: Args
): Result<T, E>;
declare function safeTry<T, E, Args extends unknown[]>(
body: (...args: Args) => AsyncGenerator<Err<never, E>, T>,
...args: Args
): Promise<Result<T, E>>;
/* --- functions returning generators --- */
function myParseInt(input: string): Generator<Err<never, Error>, number> {
const n = Number.parseInt(input);
const result = Number.isNaN(n) ? err(new Error("Parsing failed!")) : ok(n)
return result.safeUnwrap();
}
function random(): Generator<Err<never, Error>, number> {
const n = Math.random();
const result = n >= 0.5 ? ok(n) : err(new Error("Random number was too small!"));
return result.safeUnwrap();
}
function* h(input: string): Generator<Err<never, Error>, number> {
const n1 = yield* myParseInt(n);
const n2 = yield* random();
return n1 + n2;
}
/* --- usage of these functions with safeTry --- */
const result1 = safeTry(h, "42"); // ok(42 + <random number>), if the random number was large enough, otherwise err(...)
const result2 = safeTry(h, "abcdefg"); // err(...), because parsing failed
// but we can also pass myParseInt and random to safeTry directly:
const result3 = safeTry(myParseInt, "0"); // ok(0)
const result4 = safeTry(random); // ok(<random number>), if the random number was large enough, otherwise err(...) Support for passing in arguments could easily be added without a breaking change. But I believe, if we wanted to change the type of generator function that needs to be passed in according to what I described above, that would require a breaking change :/ Of course, there is ways to do this, too, with the current api, e.g., you could have the composable functions all return function lift<T, E, Args extends unknown[]>(fn: (...args: Args) => Generator<Err<never, E>, T>): (...args: Args) => Generator<Err<never, E>, Ok<T, never>> {
return function* (...args: Args) {
return some(yield* fn(...args));
}
} and use that when passing one of the composable functions to But both of these solutions are more cumbersome than what I described above, and I don't really see a downside (aside from the fact, that it would now be a breaking change). EDIT: Possibly, it could be even more more consistent/simple, if |
@ghost91- Thank you for the comment.
Although I haven't tested, technically the latter will also work, I guess. If so, this is a matter of taste. Personally I prefer the current interface, because I think you may want to return safeTry(function*() {
const sum = (yield* parseInt("12").safeUnwrap()) + (yield* parseInt("10").safeUnwrap());
if (sum >= 20) {
return err("sum is too big")
}
return ok(sum)
}) Howereve, with your proposal, because the only way to return safeTry(function*() {
const sum = (yield* parseInt("12").safeUnwrap()) + (yield* parseInt("10").safeUnwrap());
if (sum >= 20) {
return yield* err("sum is too big").safeUnwrap()
}
return sum
}) The latter looks a bit clumsy for me.
With current interface, you can compose functions like the following. function h(input: string): Result<number, Error> {
return safeTry(function*() {
const n1 = yield* parseInt(input)
const n2 = yield* random()
return ok(n1 + n2)
})
} Or, if it's still a bit cumbersome, you can write as const h = (input: string) => safeTry<number, Error>(function* (){
// The same body as above here...
}) In my opinion, we should keep the generators used in this feature from hanging around in the library's user's code. This is because it can let the users use the generators in wrong ways. The generator made by
If your motivation is to feed arguments to the body of safeTry, the |
Hey @tsuburin thanks a lot for your response! The main motivation behind the idea of composing functions that return generators directly is to reduce boilerplate in as many places as possible. With your example for
It's actually simpler, just safeTry(function*() {
const sum = (yield* parseInt("12").safeUnwrap()) + (yield* parseInt("10").safeUnwrap());
if (sum >= 20) {
yield err("sum is too big"); // or even just yield "sum is too big"
}
return sum;
}); I see your point that it might sometimes be convenient to be able to return a Result directly. But on the other hand, that is also a bit inconsistent: There are 2 ways to "return" errors, either The reason I started to investigate this at all is that I was initially very confused why safeTry(() => result.safeUnwrap()); to work. But it does not, you need to to safeTry(function* () {
return ok(yield* result.safeUnwrap());
}); which (to me) feels confusing. One other reason for modelling it the way I suggested: This whole functionality is basically Hakell's do notation for the async/await in JavaScript is actually bit special in this regard, because it allows you to do both: You can just return a plain value from an Regardless, to me it seems natural that you can return plain values and don't have to wrap them in I see that you have modeled this more after how Rust's I also get the point about not using the generators outside of safeTry(function* (unwrap) {
const a = yield* unwrap(someResult);
return ok(a); // I'd still prefer return a, though :)
}); That way, this unwrapping functionality is only available inside callbacks for When going this route, you don't even really need generator functions anymore. You can also propagate the error by wrapping it inside a specific error class, throwing that in safeTry((unwrap) => {
const a = unwrap(someResult);
return ok(a); // I'd still prefer return a, though :)
}); This solution might be an interesting alternative because of its simplicity (generators can be scary for some people). Anyways, sorry for the long rant. At least, I now have a better understanding of where you are coming from (Rust's The nice thing is: Even if people here don't agree, I can just implement my own |
@ghost91- Thank you for your reply, too. I'm not so familiar with Haskell, so your point is really interesting to me. I have some experience of Rust but not of Haskell so much. I completely agree on that it's preferable to reduce boilerplate as much as possible. However, another aspect I had in designing this feature was ease of understanding. In that aspect, I didn't really want to use generator at all. (This depends on individual, and if you argue generators are easy to understand, it is completely fair, but for me, it's not.) Actually,
this was exactly what I considered first, and gave up because I found it is very difficult to implement this
Oh, yes, this should work. But here you use
This idea seems worth considering. (Including the downside you mentioned, because it might be as obvious as this is that
As you pointed out, it is difficult (or may be impossible) to tell whether a Result instance is intended to be T or Result<U, F>, if we want to do it with the same function. However, also as you mentioned, you can implement your own function, and maybe you can make an issue or PR to hear comments from others. (I guess it is better to move to an issue if we continue this conversation, because this is a closed PR.) Anyways, again, thank you for your comments with a lot of points I haven't noticed. 😄 |
Since the dedicated class can only be thrown by unwrap, I believe it's safe to assume that the contents are correct, unwrap itself is typed so that only the correct kind of result can be passed to it. The only way I can see this breaking is with your next point, which is valid:
Here is an example how this combination could break: export function safeTry<T, E>(body: (unwrap: <U>(result: Result<U, E>) => U) => T): Result<T, E> {
try {
return ok(body(unwrap))
} catch (e) {
if (e instanceof UnwrapError) {
return err(e.error) // typed any, but we _know_ that it should be E, because the only way UnwrapError could have been thrown is from unwrap, unless users _somehow_ get a hold of UnwrapError
}
throw e
}
}
function unwrap<T, E>(result: Result<T, E>): T {
if (result.isOk()) {
return result.value
}
throw new UnwrapError(result.error)
}
class UnwrapError<E> extends Error {
constructor(readonly error: E) {
super()
}
}
const result = safeTry((unwrap: <U>(result: Result<U, number>) => U) => {
try {
const result: Result<number, number> = err(42)
const a = unwrap(result)
return a
} catch (e: any) {
const constructor = e.constructor
throw new constructor('boom!')
}
})
// result typed as Result<number, number>, but it actually is Err<never, string> :( I would argue that getting the constructor from a thrown error and then creating a new instance with different parameters is not something you should ever do, but it's possible. I think it would be acceptable to ignore this problem, because it's so obscure (and you need to use The fact that you can have a try catch inside the callback, which does not rethrow the custom error, is a much bigger problem, because try catch is still common syntax, and it completely breaks the functionality. I think this is a killer argument to not do it this way. Thanks for bringing it up, I had not thought of it (since we don't throw any errors, we always use neverthrow :D). I mostly wanted to understand the reasoning behind the design choices. I think, I have a good understanding of that now. I'll think about whether it makes sense to continue and open a new issue to continue the discussion, if needed. Thanks for your input, I really appreciate it! |
@ghost91-
This assumption is not safe, at least with your example above, because a kind of mistakes like the following is quite possible. safeTry((unwrap: <U>(result: Result<U, number>) => U) => {
const result = safeTry((umwrap: <T>(result: Result<T, string>) => T) => { // mistyped as umwrap
const r: Result<number, number> = err(3);
const x = unwrap(r); // unwrap from the outer safeTry is called
return x;
})
// result is typed as Result<number, string> but is actually Err<number, number>
}) |
Fair point. Unless I am missing something, that's solvable by creating the class inside export function safeTry<T, E>(body: (unwrap: <U>(result: Result<U, E>) => U) => T): Result<T, E> {
class UnwrapError<E> extends Error {
constructor(readonly error: E) {
super()
}
}
function unwrap<T, E>(result: Result<T, E>): T {
if (result.isOk()) {
return result.value
}
throw new UnwrapError(result.error)
}
try {
return ok(body(unwrap))
} catch (e) {
if (e instanceof UnwrapError) {
return err(e.error)
}
throw e
}
} Because then for every invocation of But at this point, it all becomes a bit weird and annoying, and I'm not sure I'm not missing any other edge cases. So probably the generator based version really is better. |
This is what I proposed in #444 , with some superficial modification to match the current APIs.
Closes #444