Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does switching from struct to enum breaks API, exactly?

Tags:

rust

I encountered an interesting change in a public PR. Initially they had:

#[derive(Debug, Clone, PartialEq, Eq, Copy)]
pub struct ParseError(ParseErrorKind);

#[derive(Debug, Clone, PartialEq, Eq, Copy)]
enum ParseErrorKind {
    OutOfRange // ... omitting other values here to be short
}

ParseError cannot be instantiated by clients because ParseErrorKind is private. They are making that enum public now, which seems ok, but I suggested an alternative: have ParseError be an enum itself, and leverage the type system instead of imitating it with the notion of "kind". They told me that would be an API breakage, and therefore was not ok.

I think I understand why in theory a struct and an enum are different. But I am not sure to understand why it is incompatible in this precise case.

Since the struct ParseError had no mutable field and cannot be instantiated by clients, there was nothing we could do with the type but to assign it and compare it. It seems both struct and enum support that, so client code is unlikely to require a change to compile with a newer version exposing an enum instead of struct. Or did I miss another use we could have with the struct, that would result in requiring a change in client code?

However there might be an ABI incompatibility too. How does Rust handle the struct in practice, knowing that only the library can construct it? Is there any sort of allocation or deallocation mechanism that requires to know precisely what ParseError is made of at buildtime? And does switching from that exact struct to an enum impact that? Or could it be safe in this particular case? And is that relevant to try to maintain the ABI since it is not guaranteed so far?

like image 523
Victor Paléologue Avatar asked Mar 28 '26 01:03

Victor Paléologue


1 Answers

That's because every struct has fields, and hence this pattern will work for any struct, but will not compile with an enum:

struct Foo {}

fn returns_a_foo() -> Foo {
  // anything that may return a Foo
}

if let Foo { .. } = returns_a_foo() {}

For example, this code compiles:

fn main() {
    if let String { .. } = String::new() {}
}

Playground.

And while probably not code you'd write on your own, it's still possible to write, and additionally, possible to generate through a macro. Note that this is then, obviously, not compatible with an enum pattern match:

if let Option { .. } = None {
    // Compile error.
}

Playground.

like image 152
Optimistic Peach Avatar answered Mar 29 '26 15:03

Optimistic Peach



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!