Stream: t-lang

Topic: try and type inference


Josh Triplett (May 04 2020 at 17:03, on Zulip):

@Taylor Cramer If I'm understanding your concern about type inference correctly, you're saying that it would surprise you if we can get away with not writing type annotations for the carrier (e.g. Result<_, _>) because you expect to need annotations for the error type?

Josh Triplett (May 04 2020 at 17:03, on Zulip):

(because of double conversion?)

Taylor Cramer (May 04 2020 at 17:06, on Zulip):

Right, I'd expect you'd need at least some annotation to tell it what error type you want

Josh Triplett (May 04 2020 at 17:08, on Zulip):

So, I don't know how feasible this is, but personally I would love it if we had some general rule for double-type-inference cases, that amounts to "convert as far as possible during the first conversion".

Josh Triplett (May 04 2020 at 17:09, on Zulip):

As in, try { x? }.context(...)? would simply not need to do any conversion the second time, because it already converted to the type it needed the first time.

Taylor Cramer (May 04 2020 at 17:11, on Zulip):

I'm not quite sure I understand-- I don't see "double conversion" as the problem

Taylor Cramer (May 04 2020 at 17:11, on Zulip):

I'm thinking of code like thsi:

Taylor Cramer (May 04 2020 at 17:11, on Zulip):

match (try { x? }) { Ok(_) => ..., Err(_) => ... }

Taylor Cramer (May 04 2020 at 17:11, on Zulip):

what should the error type there be?

Taylor Cramer (May 04 2020 at 17:12, on Zulip):

it's unknown because the error type gets into'd

Josh Triplett (May 04 2020 at 17:12, on Zulip):

For one thing, I can't really imagine a case where I'd want to match Err(_) like that without wanting to capture the Err(e). (Leaving aside that I'm going to go to great lengths to never manually write that match.)

Josh Triplett (May 04 2020 at 17:13, on Zulip):

And if I'm capturing the e it's likely to do something with it that will determine its type, such as checking an error code inside it.

Josh Triplett (May 04 2020 at 17:14, on Zulip):

I think this might actually raise a good general point: there's likely value in people talking about their desired use cases for try, how they see themselves using it, so that we can understand how those use cases would be impacted.

Taylor Cramer (May 04 2020 at 17:14, on Zulip):

I personally know that I have code that wants this:

let res = try { ... };
match res {
    Ok(()) => {},
    Err(e) => error!("Error: {}", e),
}
Taylor Cramer (May 04 2020 at 17:15, on Zulip):

That code doesn't provide any guidance as to what the type of e is

Josh Triplett (May 04 2020 at 17:15, on Zulip):

@Taylor Cramer I'm curious, why is that a match rather than a macro of some kind, for instance?

Taylor Cramer (May 04 2020 at 17:15, on Zulip):

I mean, I could also write it e.map_err(|e| error!("Error: {}", e)) if I don't care about asserting that the Ok case is ()

Taylor Cramer (May 04 2020 at 17:15, on Zulip):

does that answer your question?

Josh Triplett (May 04 2020 at 17:16, on Zulip):

...not really?

Taylor Cramer (May 04 2020 at 17:16, on Zulip):

Can you write it how you'd write that code instead?

Josh Triplett (May 04 2020 at 17:16, on Zulip):

Because if I had to write that once, let alone twice, I'd be trying very hard to factor it out.

Taylor Cramer (May 04 2020 at 17:17, on Zulip):

what would you write instead?

Josh Triplett (May 04 2020 at 17:17, on Zulip):

macro_rules! check_eprint { ... } check_eprint!(...), most likely?

Taylor Cramer (May 04 2020 at 17:17, on Zulip):

what does check_print do internally?

Josh Triplett (May 04 2020 at 17:17, on Zulip):

(Note: I don't know in practice if I'd put the try inside the check_print or in its argument. Would have to think about that.)

Taylor Cramer (May 04 2020 at 17:18, on Zulip):

to clarify: i'm trying to understand what code inside of check_print changes whether this is an inference issue or not

Josh Triplett (May 04 2020 at 17:20, on Zulip):

(typing, sorry)

Josh Triplett (May 04 2020 at 17:20, on Zulip):

(In a meeting)

Taylor Cramer (May 04 2020 at 17:21, on Zulip):

no worries, I have to get to other things soon anyways, but I wanted to offer that as one example of a place where the error type will not be inferred

Josh Triplett (May 04 2020 at 17:22, on Zulip):

So, for one thing the use case is a little hard for me to understand. Does error! print to stderr and then exit the program? Or what does it do?

Josh Triplett (May 04 2020 at 17:23, on Zulip):

Because I think I'd normally handle that by propagating the error to the top of the program and handling such errors in main.

Lokathor (May 04 2020 at 17:23, on Zulip):

I think I'm with Taylor, I often match on a Result with some branches handling the Ok and some handling the Err, and putting some extra macro around that seems like overkill.

Josh Triplett (May 04 2020 at 17:24, on Zulip):

That said, if I needed to write that macro, I'd likely use something like if let Err(e) = expr { let e: anyhow::Error = e; error!("Error: {}", e); }.

Josh Triplett (May 04 2020 at 17:25, on Zulip):

@Lokathor I have an extremely low tolerance for boilerplate.

Josh Triplett (May 04 2020 at 17:25, on Zulip):

Patterns are a sign that some function or macro or other pattern-extraction is needed.

Josh Triplett (May 04 2020 at 17:25, on Zulip):

I'm glad that the common case of error handling is reduced to ?.

Josh Triplett (May 04 2020 at 17:26, on Zulip):

And I don't want any other "handle the error if any" pattern to be substantially longer than that.

Lokathor (May 04 2020 at 17:26, on Zulip):

I would agree with your radical anti-boilerplate platform, but when the answer is "so i'll write a macro_rules" I give great, great pause.

Josh Triplett (May 04 2020 at 17:27, on Zulip):

I can understand that, and I tend to hesitate before writing non-local macros.

nikomatsakis (May 04 2020 at 17:27, on Zulip):

It seems to me that the question of a macro is really besides the point

nikomatsakis (May 04 2020 at 17:27, on Zulip):

The point is that there is going to be some code that "handles" the exception

Josh Triplett (May 04 2020 at 17:27, on Zulip):

That's part of why I observed that in the common case I would want to return the error all the way up to main, in which case I just have to write ?.

Josh Triplett (May 04 2020 at 17:28, on Zulip):

@Taylor Cramer Could you clarify what error! does in your sample code?

nikomatsakis (May 04 2020 at 17:29, on Zulip):

But even in the main case, then

nikomatsakis (May 04 2020 at 17:30, on Zulip):

the point is that it would be required to manage the exception

nikomatsakis (May 04 2020 at 17:30, on Zulip):

I believe error! though was meant to be the "error-level log" macro

nikomatsakis (May 04 2020 at 17:30, on Zulip):

i.e., Taylor Cramer's code was meant to "log the error and move on with life"

Josh Triplett (May 04 2020 at 17:30, on Zulip):

@Lokathor It also occurs to me that as long as you call try explicitly, you could then put the rest in a function rather than a macro, since it doesn't have to stop the propagation of ?..

Josh Triplett (May 04 2020 at 17:31, on Zulip):

And a function has the advantage of being able to include an expected type (e.g. anyhow::Result<()>).

Josh Triplett (May 04 2020 at 17:32, on Zulip):

So a function like fn check_eprint(result: anyhow::Result<()>) { ... } for error reporting would handle the boilerplate removal and the type inference issue.

Josh Triplett (May 04 2020 at 17:32, on Zulip):

nikomatsakis said:

the point is that it would be required to manage the exception

Right, but if it bubbles up to main it'll have a type by then.

Josh Triplett (May 04 2020 at 17:33, on Zulip):

Which means it doesn't have a type inference issue.

nikomatsakis (May 04 2020 at 17:34, on Zulip):

No, it won't

nikomatsakis (May 04 2020 at 17:35, on Zulip):

i.e., if I write

try {
    foo()?
}

the result type of that is fully unknown, today, but even if I constrain to Result<_, _>, the error type is unknown. It is independent of foo()'s return type, except in so far as it is "something that the error of foo can be converted into".

nikomatsakis (May 04 2020 at 17:35, on Zulip):

This I think is @Taylor Cramer's point, and I think certainly one of the challenges around try blocks. (The other is "chained" errors, which I think you highlighted earlier.)

nikomatsakis (May 04 2020 at 17:36, on Zulip):

(Though the two are not independent, it's just that "chained" errors -- i.e., trying to apply ? to the result of try block -- are going to lead to type inference failures as a consequence of this.)

Taylor Cramer (May 04 2020 at 17:37, on Zulip):

Right, there are a number of things you could do with the error value (including .into(), as ? again would do) that wouldn't fully constrain it

Asa Zeren (May 04 2020 at 22:33, on Zulip):

I'm wondering how common

let res = try { x? };
match res {
    Ok(()) => {}
    Err(e) => {error!("Error: {}", e)}

would be, as I see the main advantage of try is allowing things like this:

let res = try {x?};
match res {
     Ok(()) => {}
     Err(MyErrorType::Foo) => {/* handle this locally */}
     Err(e) => return Err(e) // Propagate other errors

In that case, the error type would be specified as MyErrorType (unless_::Foo is added.)

Taylor Cramer (May 04 2020 at 22:47, on Zulip):

@Asa Zeren i've personally wanted that (casing out a particular error) much less than switching to logging or adding context, or even throwing the error away entirely

Asa Zeren (May 04 2020 at 22:52, on Zulip):

If that is your use case, would it not be better to have something like:

fn log_err<R: Try(res:R)->Option<R::Output>{ /* ... */ }
Asa Zeren (May 04 2020 at 22:52, on Zulip):

Though I guess that has the same issue

Last update: Jun 05 2020 at 22:35UTC