To begin, let me make this clear: I like Rust. It's a very useful tool, despite the following complaints, and the only one i use for serious systems programming any more. (I was making my own language, but really like having some nice other people do all that hard work for me, so thank you, Rust devs!)
That said, it could be better.
The standard library has a major flaw: it aborts on failure to allocate memory. This is unacceptable in many of my use cases [1]. If it is acceptable, i am using Haskell, and trust me: you really want to not be competing with Haskell (and its ilk). It is frustrating as it would be easy to use/wrap a fallible API infallibly [2], but not vice versa.
It would not be so bad, except for all the magic in libstd: to write an alternative means various dark rituals and incantations like feature
and lang
and such which are unstable and only work on nightly. lang
in particular makes interop with crates using std
quite difficult — if you define your own Box
, you can't even link it in the same program as libstd, even if they are never in scope together!
Even absent lang items, it causes pain: see for example Vec
in my containers crate, which is essentially exactly the same as a std::Vec
— pointer, length, capacity — but is a separate uninteroperable type.
It is also mildly irritating to have to write #![no_std]
in so many crates, which becomes noise if one sees it so often — in my opinion this ought to be specified in the cargo file.
In Haskell we can define some very useful typeclasses [3][4]:
class Mappable f where
map :: (a -> b) -> f a -> f b
class Mappable f => Bindable f where
bind :: (a -> f b) -> f a -> f b
bind f = join . map f
Let's consider what these might be in pseudo-Rust:
trait Mappable {
fn map<A, F: Fn(A) -> B>(self<A>, f: F) -> Self<B>;
}
trait Bindable: Mappable {
fn bind<A, B, F: Fn(A) -> Self<B>>(self<A>, f: F) -> Self<B>;
}
Now let's consider some impl
s:
impl Mappable for Option {
fn map<A, F: Fn(A) -> B>(self<A>, f: F) -> Self<B> { ... }
}
impl<E> Mappable for Result<_, E> {
fn map<A, F: Fn(A) -> B>(self<A>, f: F) -> Self<B> { ... }
}
impl Bindable for Option {
fn bind<A, F: Fn(A) -> Self<B>>(self<A>, f: F) -> Self<B> { ... }
}
impl<E> Bindable for Result<_, E> {
fn bind<A, F: Fn(A) -> Self<B>>(self<A>, f: F) -> Self<B> { ... }
}
Oh wait, we already have these methods! (bind
is called and_then
.) But they're not a trait method, so we can't quantify over them, alas.
Say we want to define a recursive data type:
enum List<A> { Nil, Cons(A, List<A>) }
What happens:
error[E0072]: recursive type List
has infinite size
Oops, we need an indirection! Well, say we're not sure what kind of pointer we want — in some situations we want an owned pointer to a dynamically-allocated value, in others we want an atomic (e.g. for a lock-free list), whatever. This would be another use case of higher-kinded quantification, again in pseudo-Rust:
enum List<A, Ptr> { Nil, Cons(A, Ptr<List<A>>) }
Last, i wish to note the utility of data kinds (a.k.a. const generics). Look at the linea crate: we must write such hellish type signatures merely to parametrize our code over the length of an array. However, it seems const generics are in the works, so i shall say no more here.
Say we have some resource A, and we want to compute another resource B which keeps a reference to A, and keep them together in a struct
for convenience.
We can't.
At least, not in safe Rust.
I defer to tomaka for a lengthier explanation of the difficulties this poses.
I merely wish to note this bites me also, and really hurts if one is trying to do as little dynamic allocation as possible, as in much systems programming...
When doing systems programming, sometimes we must drop down to assembly. C has facilities to do so; stable rust has none. Potential reasons to want asm include the following:
- Doing a system call and not spilling my locals or using the mentally-retarded
errno
C API - Writing an operating system kernel
- Using CPU- or arch-specific performance-boosting instructions
As it is, the inline asm syntax is quite ad-hoc and baffling. This is not due to Rust — it was merely copying what came before. Nonetheless, a smoother (and preferably stable) syntax would be quite welcome. Rust has an opportunity to lead here!
[1] I want this for a few reasons:
- I often write code which must continue to operate in an out-of-memory situation, to potentially allow user intervention and system recovery. I disable overcommit to stop the kernel killing some random process and destroying state -- sometimes the process using all the memory is using it for a good reason.
- I write libraries which not presume to know better than the user how they want their program to behave in an out-of-memory situation, so any libraries they depend on must not so presume either.
[2] I write "fallible" to mean the caller can observe a failed allocation — dynamic allocation is ever fallible, but if it aborts the process on failure the caller can not observe it. I believe this is the customary use of these words in the Rust community.
[3] A typeclass is roughly analogous to a trait.
[4] These in the base library are actually called Functor
and Monad
[5], which is mathematical jargon irrelevant to this document. (They also have an intermediate class Applicative
which is useful in its own right, but not significant to this document.)
[5] Yes, you in the front in the Haskell shirt, Monad
does have another method. I omit it here for simplicity.