| I enjoyed this article. As someone who has written too much haskell and ocaml, and now writes mostly Rust, I am biased but I think this problem is mostly solved by rust. (The author mentions rust in option 3, but I think underappreaciates it.) The author mentions linear types. This is a bit of a pet peeve of mine because, while very useful, linear types are not the concept that many people think they are and they are not implemented in rust (and neither are affine types). What rust implements is referred to as a "uniqueness type". The difference has to do with how they deal with what linear-types-people call "exponentials". A linear type, as the article mentions, is the type of a value must be "consumed" exactly once (where consuming means passing it into a function, returning it, or sometimes destructuring it). Of course, in this mad world of ours we sometimes need to consume a value more than once, and indeed a language with only linear types would not be turing-complete. This escape hatch is called an "exponential", I guess because exponential is kind of like the opposite of linear. A value with an exponential type can be used as often as you want. It is essentially most types in most programming languages. IF a function expects a value with a linear type, can you pass an a value with an exponential type to it? The answer is that you can. Try this in linear haskell if you don't believe me. A function taking a value with a linear type just says "I consume this value exactly once, but you can pass me whatever you want". The restriction is that values with linear types can only be passed to functions that expect linear types. A value with a linear type must be consumed exactly once, so you certainly can't pass it to a function that expects a value with an exponential type, because it might use that value twice. In other words, a linear type is a restriction on the callee. Those familiar with rust will notice that this is not how rust works. If a function takes T, and you have &T, you just cannot call that function. (Ignore Clone for now.) However, in the world of linear types, this would be allowed. This makes linear types not useful for the article's purposes, although they are still very useful for other things. What rust wants is a constraint not provided by linear types. Where linear types are a restriction on the callee, rust wants to be able to restrict the caller. It wants to be able to restrict the caller in such a way that it can know that there are no other references to a variable that's been passed into a function. People call this a "uniqueness type" because you can say "I want this type to be 'unique'" (where 'unique' means not-aliased). Honestly this name doesn't make a lot of sense, but it makes a little more sense when you think of it in terms of references. If a reference is unique, then it means that no other reference that points to the same object (which is the requirement rust imposes on mutable references). So while a linear type allows you to pass a non-linear variable to a function that expects a linear one, rust doesn't allow you to pass a non-unique variable to a function that expects a unique one. And adding this requirement to mutations resolves 90% of the issues that make mutability annoying. Mutability becomes challenging to understand when: 1. You have multiple references pointing to the same data in memory.
2. You change the data using one of these references.
3. As a result, the data appears to have changed when accessed through any of the other references. This simply cannot happen when mutability requires values to not be aliased. |
> Those familiar with rust will notice that this is not how rust works. If a function takes T, and you have &T, you just cannot call that function. (Ignore Clone for now.)
I think this is wrong. An exponential type in Rust is a type that implements `Copy`. The analogy in Rust is:
And that compiles fine: `foo` is implicitly copied to maintain that `linear_fun` owns its parameter.You can ignore `Clone`, but ignoring `Copy` destroys the premise, because without it Rust has no exponential types at all.
EDIT: I agree Rust solves the issue of mutability fairly well. Furthermore, I think practical linear types can be added to a Rust-like type system with Vale's (https://vale.dev/) Higher RAII, where a "linear type" is an affine type that can't be implicitly dropped outside of its declaring module.
I don't know if this is what Vale does, but to enforce "can't be implicitly dropped outside of its declaring module" in Rust I would add two changes:
- Whenever the compiler tries to insert implicit drop code for a linear type outside of its declaring module, it instead raises an error.
- Type parameters get an implicit `Affine` auto-trait, like `Sized`. If a type parameter is `?Affine`, the compiler will refuse to insert implicit drop code. Standard library generic parameters will be `?Affine` wherever possible, e.g. containers like `Vec` and `HashSet` will have `T: ?Affine`, but the methods that could implicitly destroy an element like `HashSet::insert` will have plain `T`.