I've seen a bunch of people talking about trying to make a new trait for linear types (or undroppable, or unleakable), which I'm convinced would be a lot of work. However, I thought of something which might be easier to implement and still get most of the benefits: Instead of a full trait, we could make an annotation, like #[must_use]
, that raises a lint.
Any other thoughts about this approach to "linear" types anywhere? I find it hard to believe that I'd be the only one to think of this, but a cursory search didn't find anything.
My idea
We make a new attribute #[must_consume]
(can bikeshed the name and whether it belongs under the diagnostic
namespace), which can annotate a struct
/enum
/union
definition and marks the type as one which must be consumed.
For the purposes, of this attribute, "consuming" the value can be either destructuring it or calling a method (only methods, not other functions) on that value which 1) takes ownership of the value, and 2) is defined in the same crate as the value.
As with #[must_use]
you can also supply a string message to be displayed in a hint if a value is not consumed. Unlike #[must_use]
, a simple let _ =
does not suffice to silence the lint (the only workaround is an #[allow(..)]
where the value is dropped).
For example, with the following definition:
#[must_consume]
struct Foo {
pub bar: u8,
baz: u8,
}
The following function, defined in the same module as Foo
is allowed because it destructures the Foo
value:
fn destruct_foo(foo: Foo, noisy: bool) {
let Self { bar, baz } = self;
if noisy {
println!("Destructing `Foo { bar: {bar}, baz: {baz} }`");
}
}
This destruct_foo
function would also be allowed outside of the module, if baz
were a public field.
This method, also defined in the same module as Foo
, is also legal because it's a method on Foo
that takes ownership and is defined in the same crate:
impl Foo {
fn consume(self) {
println!("Consuming `Foo { bar: {}, .. }`", self.bar);
}
}
However, replacing this method with a free function or writing this method outside of the crate defining Foo
(e.g. through a trait implementation) would trigger the lint.
Because Foo
has a private field, any Foo
values created outside of the defining crate can only be destroyed by passing the value into the destruct_foo
or Foo::consume
functions defined in the crate. Note that it is allowed to do so indirectly, so e.g. these functions are allowed:
use foo::Foo;
fn take_foo(foo: Foo) {
foo.consume();
}
fn take_foo_2(foo: Foo) {
take_foo(foo);
}
Edge cases
If a value is wrapped in another value (e.g. a struct containing this type as a field), then it should still warn if the outer value is dropped, as that drops the inner value.
I'm not sure how easy this would be to do, but it'd also be nice if this applied through generic types, so you can't accidentally ignore this by sticking the value inside a wrapper type. The analysis can't perfectly extend through Rc
and other types that may or may not drop values based on runtime behavior, but ideally we'd get as many ways of dropping a value as possible.
Limitations
The main limitation is that, as a lint, a library author can't stop users from #[allow(drop_must_consume)]
(or whatever we call the lint), hence why I titled the post "linear-ish types", as it only incentivizes correct use, it doesn't require it. Thus, unsafe
code can't rely on one of these functions being called for soundness (any code can rely on it for correctness, though the stdlib probably doesn't want to).
Also, this doesn't truly enforce linear types, even without lint allow
s, since you can e.g. make an Rc
cycle or write to a static
variable to leak the value.
Cases where it helps
Async Drop
This would help with the async Drop
problem, as you can raise a warning when people hit the drop
code. For example:
#[must_consume]
struct Session { /* private members */ }
impl Session {
async fn close(self, cx: ..) { .. }
}
This will heavily encourage library users to call Session::close
to gracefully close a session when they're done with the session, instead of dropping the handle. It's not a guarantee that it will get called (it could get leaked, or the lint could be allow
ed), but this makes it harder to accidentally misuse the Session
type.
Return from Drop
There's a lot of types that do something like this:
struct IoResource { /* private members */ }
impl IoResource {
/// Close the resource because we're done with it.
fn close(self) -> io::Result<()> { .. }
}
impl Drop for IoResource {
/// Attempt to close the resource.
///
/// Prefer [`Self::close`] because it can return an error if something goes wrong.
fn drop(&mut self) -> { .. }
}
If the type is now annotated with #[must_consume = "Close the `IoResource` by calling `close` to allow handling an error"]
, then users will get a lint unless they explicitly call IoResource::close
on the value (up to the caveats mentioned above) suggesting that they call the close
method instead (which returns a Result
which is #[must_use]
, so the caller must also decide how to handle that).
If you want to be even more pedantic about closing the resource, you could even do:
fn close(self) -> Result<(), (io::Error, Self)> { .. }
to force the caller to retry closing the resource until it succeeds (probably not good API design most of the time, but you can do it).