Skip to content

Add Erased Convert Error Type #644

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

Merged
merged 1 commit into from
Apr 7, 2024

Conversation

Dheatly23
Copy link
Contributor

Erased error type is useful to separate non-sendable values in ConvertError with sendable error type. But the trade-off is some data might be gone in conversion.

For now it does not report the value, though this might change. Also in the future ConvertError can be made richer without sacrificing eg. sendability.

@GodotRust
Copy link

API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-644

@Bromeon
Copy link
Member

Bromeon commented Mar 12, 2024

Generally these error APIs are not very sophisticated by design, as they add complexity in a part of the library that hardly benefits from it. And I'd definitely like to keep the API clutter down to a minimum (e.g. additional type ErasedConvertError).

Erased error type is useful to separate non-sendable values in ConvertError with sendable error type. But the trade-off is some data might be gone in conversion.

Can you demonstrate an example that requires it? Rust's Error trait is generally designed around errors being Send, so if possible I'd like to stick with that.

Also keep in mind current changes in #645 which is a bugfix.

@Bromeon Bromeon added quality-of-life No new functionality, but improves ergonomics/internals c: core Core components labels Mar 12, 2024
@Dheatly23
Copy link
Contributor Author

Dheatly23 commented Mar 12, 2024

I thought it's a bit better implementation of #525 , by separating error type into sendable and non-sendable part (since Godot types are currently non-sendable). I noticed the former is slow (writing string), particularly if conversion is retried again (eg. multiple dispatch). Kinda work around it, but it's not satisfactory IMO. And now that i have some free time, i can work it to my satisfaction 😅

With naming, it can be worked out later. It's definitely not SendableConvertError because some data is lost (like value).

@Bromeon
Copy link
Member

Bromeon commented Mar 12, 2024

since Godot types are currently non-sendable

But can you show concretely in which situation this is a problem, with code?

Values are currently erased via fmt::Debug, and the cause is supposed to be another error (i.e. recursively has the same requirements as this one).


With naming, it can be worked out later

I'm not concerned about naming, but about APIs becoming more complex than necessary. See also here for detailed reasoning.


I noticed the former is slow (writing string), particularly if conversion is retried again (eg. multiple dispatch)

Now I'm curious, how did you notice it, did you measure it? Not that I don't believe it, but I'm wondering if this is something worth optimizing 🙂

@Dheatly23
Copy link
Contributor Author

But can you show concretely in which situation this is a problem, with code?

Values are currently erased via fmt::Debug, and the cause is supposed to be another error (i.e. recursively has the same requirements as this one).

Casting a sufficiently complex value should suffice. Probably not very heavy, but Debuging that probably isn't very efficient.

Now I'm curious, how did you notice it, did you measure it? Not that I don't believe it, but I'm wondering if this is something worth optimizing 🙂

Mostly because i had configuration struct that can be multiple types (int/float, string/bytes, file, (non)null, etc). Multiply that by however many values, and it's hitting pretty hard. Especially if the actual value is long to print (like an entire file in PoolByteArray or GString).

If there's a VariantDispatch it can be way faster. Though typed array can throw a wrench in that 🙃

@Bromeon
Copy link
Member

Bromeon commented Mar 12, 2024

Fair enough, however this alone doesn't justify making the API more complex. Even if we use an erased value (as Variant or VariantDispatch), I would not add a separate ErasedConvertError. This can be kept internal.

And removing Send + Sync also has its price, but it might (?) be OK given the benefits.

Copy link
Member

@Bromeon Bromeon left a comment

Choose a reason for hiding this comment

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

Would be nice if you could rebase it on top of #645.

If you add type erasure, why not use Option<Variant> directly as the value? dyn Debug is less useful...

Please also keep API changes to a minimum. Would be cool if we didn't need a separate ErasedConvertError 🙂

}) if with_source => format!(
"\n Source: {}{}{}",
e,
if v.is_empty() { "" } else { ": " },
Copy link
Member

Choose a reason for hiding this comment

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

Please use variables here.

Comment on lines +388 to +391
Convert {
erased_error: ErasedConvertError,
value: String,
},
Copy link
Member

Choose a reason for hiding this comment

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

I think it would be nicer to have single ConvertError, no erased one. Then this enumerator could be kept simple, as well.

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 won't compile because ConvertError is no longer Send, so i have to work around that.

@Dheatly23
Copy link
Contributor Author

Would be cool if we didn't need a separate ErasedConvertError 🙂

Should the type be hidden from user? It can be accessed by fn into_erased(self) -> impl Error + Send + Sync

@Dheatly23 Dheatly23 force-pushed the erased-convert-error branch from 87f915e to 3093bac Compare March 13, 2024 02:55
@Dheatly23 Dheatly23 marked this pull request as ready for review March 13, 2024 03:47
@Bromeon
Copy link
Member

Bromeon commented Mar 13, 2024

At the risk of repeating myself 😀

Can you demonstrate an example that requires it?

But can you show concretely in which situation this is a problem, with code?

It would be good to justify any addition to the API (here ErasedConvertError) with a motivating use case. And showing a short code snippet is often more expressive than long descriptions. You may have encountered scenarios in your projects that make this valid, but don't assume everyone in this thread has done the same.

I understand the technical limitation (general errors not being Send+Sync, erased ones sacrificing fields in order to be that) but I'm not sure if this separation occurs often enough in practice to justify two types. Maybe it's fine to just not have Send + Sync on this type, and reconsider the decision once users run into problems?

@Dheatly23
Copy link
Contributor Author

The (slow) code looks something like this:

if let Ok(v) = GString::try_from_variant(&var) {
    // Argument is a text
} else if let Ok(v) = PackedByteArray::try_from_variant(&var) {
    // Argument is a binary data
} else if let Ok(v) = <Gd<FileAccess>>::try_from_variant(&var) {
    // Argument is a file
} else if let v = <Gd<SomeType>>::try_from_variant(&var) {
    // Let's clone from our type instead
} else {
    // Return an error
}

And that's not the worst one. Let's say there are 8 Packed*Array types to handle. I kinda had to manually implement variant dispatcher.

You're right though, it's not strictly necessary. It's just a ok addition, with some edge case making printing and discarding the same error 8 times.

@Bromeon
Copy link
Member

Bromeon commented Mar 13, 2024

As mentioned, I agree about the performance. But my last question was specifically about ErasedConvertError -- how would that one make the above code more readable?

Minor thing, you can use variant.try_to::<T>() instead of <T>::try_from_variant(variant).

@Dheatly23
Copy link
Contributor Author

As mentioned, I agree about the performance. But my last question was specifically about ErasedConvertError -- how would that one make the above code more readable?

For readability, it's almost unchanging. Except for error conversion.

let s = String::try_from_variant(&v).map_err(|v| v.into_erased())?;

There can be a convenience trait impl for Result<T, ConvertError> for easier use.

Oh and btw, the erased type is no longer public.

@lilizoey
Copy link
Member

Did you consider making ConvertError generic to fix this instead? maybe something like

pub struct GenericConvertError<V> {
    kind: ErrorKind,
    value: Option<V>,
}

pub type ConvertError = GenericConvertError<Variant>;
pub type ErasedConvertError = GenericConvertError<String>;

I'm not sure if this is better, mostly just a question. It does avoid a separate type for ErasedConvertError though.

@Bromeon
Copy link
Member

Bromeon commented Mar 29, 2024

As I understood, the chain that led to the proposed solution is:

  1. Stringifying variants on each error construction is expensive (compounded by if/else chains).
  2. So, instead of the string Debug representation, let's store the original Variant.
  3. This can no longer be Send/Sync because some variant types aren't thread-safe.
  4. Thus we need two types: an original one without Send/Sync, and an erased one with.

We will likely face the same problem in any error that includes values (could e.g. affect CallError if we decide to incorporate argument values). But I also don't want to overengineer a super-general solution here, maybe just think a bit ahead so we don't have to build a specific hack again next time.

We have different concrete errors like ConvertError, CallError, IoError etc -- what if they can all be converted into a ErasedError that's basically implementing std::error::Error + Send + Sync + Debug + Display + 'static? I'm not sure if that's worth it for just Send/Sync alone, but maybe someone sees other benefits?

@Bromeon Bromeon force-pushed the erased-convert-error branch from 3093bac to 646630a Compare April 6, 2024 15:06
@Bromeon
Copy link
Member

Bromeon commented Apr 6, 2024

Rebased.

@Dheatly23 any update here? I'd like to consolidate some of the errors soon -- so even as-is, the PR would be good as a first design on which we can iterate.

Could you also fix the tests?

@Bromeon
Copy link
Member

Bromeon commented Apr 6, 2024

(I quickly need to test something, will close and reopen)

@Bromeon Bromeon closed this Apr 6, 2024
@Bromeon Bromeon reopened this Apr 6, 2024
@Bromeon Bromeon force-pushed the erased-convert-error branch from 646630a to c6fa0f1 Compare April 6, 2024 16:18
@GodotRust
Copy link

API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-644

@Dheatly23
Copy link
Contributor Author

Dheatly23 commented Apr 6, 2024

Regarding tests, i don't understand why it fails. For example, i don't touch anything close to test object_from_invalid_instance_id, yet some assertion failed. Still trying to trace the call.

Edit: (Kinda) found it. It apparently tries to clone invalid object. Maybe it can work if it skips calling if pointer is null.

@Dheatly23 Dheatly23 force-pushed the erased-convert-error branch from c6fa0f1 to 6a39968 Compare April 6, 2024 22:28
@Dheatly23 Dheatly23 force-pushed the erased-convert-error branch from 6a39968 to c2514d8 Compare April 6, 2024 23:00
@Bromeon
Copy link
Member

Bromeon commented Apr 7, 2024

Thank you!

I'll merge this for now, but keep in mind that the API may still evolve once we try to unify the different error types. Maybe there's a way to avoid the type-state pattern for this specific error 🙂

@Bromeon Bromeon added this pull request to the merge queue Apr 7, 2024
Merged via the queue into godot-rust:master with commit 18147d1 Apr 7, 2024
@Dheatly23 Dheatly23 deleted the erased-convert-error branch May 30, 2024 03:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c: core Core components quality-of-life No new functionality, but improves ergonomics/internals
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants