Repository navigation
RFC: Add linear type facility #814
Description
Activity
- addedpostponedRFCs that have been postponed and may be revisited at a later time.RFCs that have been postponed and may be revisited at a later time.
on Feb 5, 2015 This may be useful to support
closemethods for I/O objects that need error reporting when being disposed of. See a discussion thread on I/O issues.Are there any plans for when we take this up again?
#523 seems to be related, by the way.
@Munksgaard I've factored some of this RFC out into #1180, and (opened a discussion on internals about another part)[https://internals.rust-lang.org/t/pre-rfc-excdrop-trait-for-different-drop-glue-during-unwinding/2242]. I've been working through an implementation of #1180, but real life is taking too much time to make any progress at all for probably the next month or so.
Aside from fallible
Dropconstructors (e.g. closing a file), there’s another situation where it could come in handy: Sometimes an resource is held by another thread/process/host and dropping would require waiting for for the other side to complete whatever they are doing. Implicit drops would cause cause the program to block, possibly unintentionally (which can further lead to unintended deadlocks). Worse, the protocol might not even offer a way to wait for it, in which case there’s nothing you can do but panic. In these kinds of cases, it would have been better to have simply caught it as a compile error.I think linear types are suited for transient, active things that need to be dealt with carefully and explicitly, whereas affine types are more like passive objects that could be thrown away without regard. Which one is more suitable for a particular resource is probably a bit subjective and dependent on the robustness and reliability demands.
It might even be possible to have a rudimentary lint-based implementation of linear types: if an object gets dropped and the destructor unconditionally panics, then that’s almost certainly a bug and should be warned.
Reacted by Martin Pärtel, l0calh05t and Felipe S. S. SchneiderIt might even be possible to have a rudimentary lint-based implementation of linear types: if an object gets dropped and the destructor unconditionally panics, then that’s almost certainly a bug and should be warned.
How would that work? Presumably when people say linear types they don't mean types with values with an infinite lifetime, but rather that you need to pass them to some special destroy function by value when you're done with them. If you have a
Dropimpl that always panics you can't even do this (safely).How would that work? Presumably when people say linear types they don't mean types with values with an infinite lifetime, but rather that you need to pass them to some special destroy function by value when you're done with them.
It’s not infinitely long, but the time at which it becomes destructible is not statically known, and may require the user to take certain actions before it becomes destructible.
If you have a
Dropimpl that always panics you can't even do this (safely).Yes, panicking in
dropis really awkward, so I’m hoping linear types could help obviate the need for such workarounds.- addedT-langRelevant to the language team, which will review and decide on the RFC.Relevant to the language team, which will review and decide on the RFC.
on May 13, 2017 6 years now, is this ever gonna be considered ?
There have been a bunch of times when I've wanted something like this.Reacted by l0calh05t, gueltro, Lix Zhou, Drew Crawford, Dylan Ede, therocode, N.E.C., kafji, Lapshin Dmitry, Kolsky and 10 moreThis is so important! If this would be supported, rust could be the primary language to compile to cairo bytecode...
Reacted by Dzmitry Malyshau and Felipe S. S. SchneiderUse cases:
- Async drop. Instead of implementing async drop, you can forbid drop for the type and implement async fn method that must be called instead of dropping.
- Drop with arguments. Imagine a handle, that is created from some resource manager and needs a reference to this manager on drop. One way is to store the reference inside the handle (which can be tricky/expensive with Arcs/Mutexes/etc.), but the other way is to make drop accept the reference to the resource manager. With this you can forbid drop and implement custom function that accepts additional arguments.
- Drop that can fail/can have side effects. Again, the same idea, you can forbid drop and write a function, which will return error, for example, if drop failed, so it will be the job of the programmer to handle this error correctly
You can already write this functions and use them, but I think the biggest problem is that if you forget to call them, the compiler will place the default drop (and drops can be hidden in some function calls). And I understand that there is a big problem with handling this custom drops during panic, so may be add cheaty linear type:
Basically instead of just banning the drop, may be add an 'annotation' to the compiler, saying "do not add drop for this type during compilation; instead, throw compile error". So, an example:
{ let a = Foo::new(); // compiler 'will add' std::mem::drop(a); }
Instead, if the type is annotated as "no drop", in this snippet compiler will just throw an error. This can help with cases 1 and 3: in async case, we can block in the drop; in the drop-can-fail case, we could just ignore an error (because it will be called only during panic and something already went wrong). And even in the second case some one can be fine with resources "leaking" from resource manager during panic.
I know this is an error prone way to do this, but it kinda doesn't violate rust rules. You can already write all that code (blocking, ignore an error, do nothing with custom resource manager) in safe rust, but yes, you shouldn't, and the lack of the no-drop kinda forces you to find a better way. But I think this "annotation" could help in a lot of cases, where there is just no better solution.
Reacted by Dzmitry Malyshau, David Spies, Alexey Gerasev and David HoppenbrouwersI've found that using
must_usein combination withforbid(unused_must_use)is effective as a crappy form of linear types, e.g.:#![forbid(unused_must_use)] #[derive(Debug)] #[must_use = "Must be manually destroyed with `T::destroy()`"] struct T; impl T { fn destroy(self) { std::mem::forget(self) } } impl Drop for T { fn drop(&mut self) { if !std::thread::panicking() { panic!("Must be destroyed with `T::destroy()`"); } } } fn main() { dbg!(T); }
error: unused `T` that must be used --> src/main.rs:22:5 | 22 | dbg!(T); | ^^^^^^^ | = note: Must be manually destroyed with `T::destroy()`If you replace
dbg!(T)withdbg!(T).destroy()it compiles and works as expected.EDIT: in hindsight it doesn't work if you have e.g.
f(&self)and callT.f(). Ah wellWhere does the word "linear" come from? Is there any prior work in other languages for reference?
Linear types are a form of substructural type systems, notable implementors are Clean and Haskell (via GHC extension)
Reacted by SOFe (Jonathan Chan Kwan Yin) and Js2xxx@SOF3 The word "linear" comes from the notion of linear logic. I believe the concept, and the terminology, were first proposed in a 1987 paper Linear Logic by J.-Y. Girard. The terminology was likely motivated by two related concepts, coming from the theory of vector spaces and linear operators. People often talk about "linear relations" or "linear terms" when variables occur only once in the formula, by analogy with linear functions, which have a single occurrence of each coordinate variable in their expression. In linear logic, an element of each type can be used only once in the formula, because construction of pairs
(x1, x2): X \times Xis forbidden for general types. Linear logic doesn't allow duplication of values (unless special operators are used). Similarly, linear logic doesn't allow silent discarding of values: if you have an elementx: X, you must explicitly thread it through all formulas, and can't just "forget" to use it.The second, more conceptual, reason to call this logic "linear" is that it arises as internal logic of the additive symmetric monoidal category of vector spaces. Basically it is the only way your logic could be structured if vector spaces were all you knew about, and it allows to prove all facts about them. The categorical model served as the guiding star for the formulation of logic, but for practical applications the later found model of resource semantics is much more relevant.
Reacted by SOFe (Jonathan Chan Kwan Yin) and l0calh05tAny progress on this? What is it blocked on? This would be incredibly useful for interfacing with externally handled buffers, such as the ones present in a GPU. Other alternatives require having a device around to be able to destroy the resources, which unnecessarily adds bloat to the structure or to the global namespace.
I'd be willing to work on it if the PR gets considered.
Reacted by 8573, Rynco Maekawa and Psionic K@aleokdev Unlikely to happen. I think @Gankra's blog post "The Pain Of Real Linear Types in Rust", that for some reason is not linked in this thread, killed the feature for many: https://faultlore.com/blah/linear-rust/
Reacted by Alejandro Perea, SOFe (Jonathan Chan Kwan Yin) and Rynco MaekawaI will argue that we are overemphasizing the cost of an overly ambitious implementation when the real implementation we need supports almost nothing but prevents the very real problems of in-flight things being silently dropped. The contract we aim to create is not an unreasonable one, that no envelope may be licked unless it is sealed and sent.
https://faultlore.com/blah/linear-rust/
There would need to be some special handling if <T: Copy>It may not be a contradiction to split the licked envelope so long as both parts are eventually sealed and sent. However, if the semantics of
MustConsumetend to involve some external service holding an imperiledLickedEnvelope, isn't it more natural to expect the user to tell the external service to split the real external envelope before requiring the program to express that both parts of its representation will be consumed? If so, isn't it natural for the compiler to requireCloneandCopyto be manually implemented for anyMustConsumesince the user will need to notify the external service before any two such imperiledLickedEnvelopescan be made to exist? Wouldn't the compiler's refusal to implementMustConsumeandCopyautomatically be a strong suggestion to the user that these two ideas are orthogonal, and isn't that suggestion accurate?A bunch of the standard library will need to be “upgraded” to have ?Leave support.
I don't buy this. I would be fine with all of std disallowing
MustConsumethe same way many types disallow all butSized. It is easy enough to hack awayMustConsumetemporarily with some other outerMustConsumecontract on a container that usesVec<MustConsumeErased>internally. If I'm writing APIs for the user, this maneuver is merely some harmless boilerplate that a derive crate would swiftly conceal while maintaining soundness.I think there's a real case that we're tunnel-visioning on the hard implications that are not meaningful to actual use cases for
MustConsumewhile ignoring the real harm to users when imperiledLickedEnvelopesare silently dropped or only explode at runtime in the rare paths that were meant to recover. Like thread locals or branded lifetimes,MustConsumeis the kind of semantic that I want automation for when the flow control is naturally going to constrain the problem to a much smaller surface to defend.People who say they want Linear™ Types In Rust actually just want Proper Support For Relevant™ Types.
In the case of a panic, I may still want to implement custom
Dropbehavior that may clean up a runtime condition my program was not allowed to express, enabling imperiledLickedEnvelopeto be rescued on the remote service. If this counts as using my type at-least-once (once for theMustConsumethat may have failed and once in a panic hook) then I would consider Relevant types sufficient.This would be incredibly useful for interfacing with externally handled buffers, such as the ones present in a GPU.
I have encountered this need when dealing with this same need for a contract. In stable Rust, I can warn and I can explode for the user at runtime, and neither one of these things are correct. When we swear to do it all right in the name of correctness, let us not forget the errors that already happen and the robustness of conservative solutions that only support the vary narrow case.
What I'm looking at in the meantime is this perhaps dirty thing. If it saves my users from writing programs that drop an already recorded command buffer and no other implementation would demand any different semantics on the public API, that's a win.
Reacted by David HoppenbrouwersOut of pure curiosity, because I'm just a normal programmer usually using some programming language, and not fully understanding why it works or how.
Wasn't this accepted here https://github.com/rust-lang/rust-project-goals/blob/main/src/2026/move-trait.md as a rust goal and isn't it being tracked here? rust-lang/goals#635As far as I understand linear types differs from Rust implementation in this way
Rust: At most once
Linear: Exactly once.Wouldn't auto !Forget trait solve this problem?
Rendered
Tracking issue for postponed PR #776