
The Pain of Real Linear Types in Rust - Gankro
https://gankro.github.io/blah/linear-rust/
======
cwzwarich
A lot of the awkwardness that the author describes comes from destructors,
which Rust has taken from C++. In fact, Rust has even inherited the
incoherence between destructors and exceptions from C++, due to the lack of a
solution to the double-throw problem and the need to write unsafe code that is
correct in the face of unwinding.

The 'dropck' pass is one of the corners of the language that has no precedent
in a type system that has been proven sound (at least as far as I am aware,
someone please correct me if i'm wrong), and it has had a lot of soundness
issues in the past.

The fact that destructors have magical powers that the language refuses to
bestow on ordinary functions is a bad sign. And destructors are terrible for
predictable code: the order in which destructors run for temporary results in
a single expression is not even specified by the language, and there are some
surprises ([https://aochagavia.github.io/blog/exploring-rusts-
unspecifie...](https://aochagavia.github.io/blog/exploring-rusts-unspecified-
drop-order/)) that make it harder to write correct unsafe code.

If you were to design a language from the ground up with linear types and no
destructors, it would be dramatically simpler than Rust.

~~~
Animats
Destructors seem to be a pain point for functional programming people. They're
inherently imperative; they don't return anything because they have no one to
return it to.

The "unsafe code" problem comes mostly from backpointers. If you have a data
structure with a doubly linked list, and the forward pointer and backpointer
are both pure references and can't be null, no order of destruction is
strictly valid. You can't create, destroy, or manipulate a doubly linked list
or a tree with backpointers in safe Rust. That's a problem.

Maybe forward pointer/backpointer pairs need to be a language level concept.
The compiler needs to know that the forward pointer and the backpointer are in
a relationship. The pair needs to be manipulated as a unit. You have to have
mutable ownership of both references to manipulate either. The borrow checker
and destructor ordering need to understand this.

~~~
mafribe

       Destructors seem to be a 
       pain point 
    

The problem is not so much typing as such (things that don't return anything
but terminate -- as destructors do -- can be typed as Unit) but rather to find
a good _trade-off_ between expressivity of the language and simplicity of the
typing system.

Basically explicit destructors mean the typing system needs to track lifetimes
and ownership in some form or shape. There seem to be two main options.

\- _Simple lifetime /ownership scheme_, but then you need a garbage collector
anyway, and that it's mostly pointless to have explicit destructors. Just let
every variable be cleaned up by the GC makes for a simpler language (under the
hood clever escape analysis might be used for stack allocation of variables
that don't escape their activation context).

\- _Avoid a GC_ , but then you need a complex typing system with unique owners
to have any chance at expressivity (and you still need unsafe blocks and
reference counting). This is Rust's choice.

Another issue is how consistently to combine destructors with other effects,
in particular exceptions.

    
    
       Pointer/backpointer pairs 
       need to be a language level 
       concept.
    

As "JoshTriplett" also suggests, this is certainly an interesting idea, but I
don't think a compelling choice has been found yet.

~~~
Animats
Think of backpointers as a combination of Rust optional pointers and weak
pointers, mostly checked at compile time. The basic rule for backpointers is
this: If an type instance A contains a backpointer P1, it must either be a
None, or a reference to a type instance B which has exactly one reference P2
to A.

Checks required:

\- P2 cannot be changed when P1 is not None. (Run-time check; the compiler has
to recognize when it is necessary.)

\- P1 can only be set to None or B. (Compile-time check)

\- P1 must be set to None before B is destroyed. This avoids a dangling
pointer. (Compile-time check when possible, otherwise run-time check.)

\- Borrow checking must treat a borrow using P1 as a borrow of B.

These simple rules would maintain the invariant for the backpointer. This
allows doubly-linked lists without unsafe code. The backpointer is "weak" and
doesn't count as ownership. It's basically weak pointers with a count of
either 0 or 1.

~~~
mafribe
I'm not saying this can't be done, au contraire! Indeed cost coherent
programming idioms can be converted into typed language primitives. But there
is a price to pay in terms of typing system complexity.

It's a slippery slope argument: if you add this, why stop there? Especially if
you require run-time checks.

If there was a compelling set of operation that preserved the invariants
without run-time checks, and it was expressive, i.e. it covered a large number
of cases that you'd otherwise had to put into "unsafe" and it didn't ruin type
inference ...

------
kibwen
Because it's not entirely clear from the title and introduction, this essay is
actually arguing against extending Rust's type system to include linear types,
rather than critiquing the type system as it currently exists (which people
often colloquially describe as having "linear types" even if it technically
doesn't).

EDIT: The intro has been updated to be clearer, so now I just look like an
idiot. :)

~~~
mafribe

       arguing against 
    

The argument doesn't come from a position of deep understanding of
substructural types. Once you have affine types (as Rust does), linear is not
really a major step. Whether it's worthwhile from a pragmatic POV is a
different question.

~~~
kibwen
Pragmatic concerns appear to be the bulk of this argument against them.

~~~
mafribe
Such questions are best answered _after_ MLoC or GLoCs have been written.

~~~
kibwen
That may be true if your goal is to advance programming language research, but
if your goal is to extend a language being used in industry then you don't
have the luxury of spending that sort of design and implementation effort if
you suspect that the extension will be a boondoggle (especially since
backwards-compatibility promises mean that you'll be forced to support those
features for the rest of time).

~~~
mafribe
I agree.

I wonder if Rust's compile-time meta-programming can be used to implement
'pluggable' linear-types for experiments, maybe along the lines of [1]. That
might be a good compromise.

[1] S. Chang, A. Knauth, B. Greenman, Type Systems as Macros.
[http://www.ccs.neu.edu/home/stchang/pubs/ckg-
popl2017.pdf](http://www.ccs.neu.edu/home/stchang/pubs/ckg-popl2017.pdf)

~~~
kibwen
I can offer no proof but I'm Pretty Sure that one could hack together linear
types using procedural macros, though I can't posit how nice they would be to
use nor how well they would interact with the rest of the language and
ecosystem.

~~~
mafribe
Wouldn't that be a good path: start with initial experiments based on types-
by-macros, and see how well that works. Can't be worse that C++'s template
meta-programming.

------
lmm
As a Scala user: the further I've got into a functional/MLey style the more
linear my code has become. Options or collections are very naturally handled
with "fold" (cata). Loops probably shouldn't be infinite - if you're looping
it's usually because you're folding along a data structure, and those ought to
be finite (one of the ideas I'm toying with is a type-level natural indexed
recursion-schemes like library, to make it easy to construct recursive
structures that are known-finite).

I don't know if it's where Rust wants to be, but I want a practical-oriented
ML-family language that does this.

~~~
burntsushi
Friendly challenge (because I agree with you, but constantly run into
limitations):

I loop over strings a lot. Can I fit them into a nice recursive structure
without runtime overhead?

Bonus round: My loops over strings often aren't straight-forward one-byte-at-
a-time iterations. Sometimes my loops look at 8 or even 16 bytes in a single
iteration. How does that fit in with more sophisticated types like you're
describing?

~~~
lmm
In practice, probably not.

In theory, there's no reason one couldn't compile a linked list in source to a
flat array at runtime if the access pattern was right (and linear types should
make that kind of optimization a lot more practical. This might even be
something one could implement "in userspace" in a language with linear types -
like a safe version of an iterator. Certainly I'd be excited to try - I'm not
claiming this is immediately production-quality). Or one could ask where the
string is coming from in the first place, and trust some kind of fusion-style
optimization to remove the intermediate datastructure entirely at runtime.

Peeling off the front 8 or 16 elements should be no harder than peeling off
the front 1, though naively you might have to handle each partial case from 1
to 15.

~~~
burntsushi
> Peeling off the front 8 or 16 elements should be no harder than peeling off
> the front 1, though naively you might have to handle each partial case from
> 1 to 15.

I think I under-specified my requirement. :-) By "16 bytes at a time," I mean,
"run a single CPU instruction on those 16 bytes."

But yeah, I get your drift. I can see how it might be theoretically possible.
I suppose the key gains might be in how much confidence a programmer can have
that their code compiles down to the right set of instructions.

~~~
lmm
Ultimately what we want is a representation that makes it easy to understand
the performance characteristics of a given piece of code (and I'd agree that
this is an area where the purist functional tradition has lagged behind). In
the past assembly was this, but I don't know that it can be these days; I've
read that on modern hardware, the biggest factor affecting performance is
cache efficiency. So even seeing the sequence of machine instructions might
not be enough to allow a developer to reason about performance, because two
similar-looking sequences of CPU instructions can easily end up having very
different performance characteristics (branch prediction can have a similar
effect AIUI).

In the long term I hope for a "nice" language (in particular one in which
checked correctness is easy) with explicit performance semantics along the
lines of
[http://www.cs.cmu.edu/~rwh/papers/iolambda/short.pdf](http://www.cs.cmu.edu/~rwh/papers/iolambda/short.pdf)
. But yeah under the current state of the art there is no ideal approach, so
there's a tradeoff in practice even though I don't think there should be one
in theory.

------
adamnemecek
CMU has a class on Substructural Logics
[https://www.cs.cmu.edu/~fp/courses/15816-f16/](https://www.cs.cmu.edu/~fp/courses/15816-f16/)

Up until 2012 it was just about Linear Logic IIRC.

------
johncolanduoni
I was actually thinking about this precise issue this morning (due to this
GitHub issue[1]). I think a valuable middleground would be to include this
kind of check even without the ?Leave stuff. In essence, the compiler could
just give an error if it would have needed to issue a Drop call anywhere for
the type.

This ability is pretty useful when dealing with destructors that need context
that you don't want to always wrap with the given type (sometimes for
performance reasons).

[1]: [https://github.com/gfx-rs/gfx/issues/1216](https://github.com/gfx-
rs/gfx/issues/1216)

------
Animats
The author tries too hard to avoid using the word "object".

"Must use" objects don't seem to be all that useful. More justification is
needed. Is the author thinking of Javascript-like callback approaches, broken
"promises", and such?

~~~
moosingin3space
Rust's Result type is a "must use" type, which makes it impossible to skip an
error check.

~~~
Animats
Can you assign it to a variable and then ignore it?

~~~
steveklabnik
Yes, assignment counts as a use.

That will get you an "unused variable" warning though, which you can suppress
by binding to _ instead.

------
haskellandchill
> It's poorly named, and so are most of the concepts it introduces

Hostility towards academia _check_

Anyway it's fine not to have proper linear types in Rust, but I don't think
linear types are the enemy here.

~~~
hyperpape
Right or wrong, simply saying something is poorly named doesn't show that
someone is hostile towards academia. By that standard, most of the academics I
know would be hostile to academia, since they invariably think something in
their field has a bad name.

~~~
johncolanduoni
I'm just miffed that they're complaining about names that are actual words.
How would you like if they were all named after their discoverers, and
therefore an a priori undistinguished mishmash of proper names? (cf.
separation axioms[1])

[1]:
[https://en.wikipedia.org/wiki/Separation_axiom](https://en.wikipedia.org/wiki/Separation_axiom)

~~~
etatoby
There are only two hard things in Computer Science: cache invalidation and
naming things. —Phil Karlton

oh, and:

The first step towards wisdom is calling things by their right names.
—Anonymous Chinese Proverb

~~~
wtetzner
There are only two hard things in Computer Science: cache invalidation, naming
things, and off-by-1 errors.

