My Rust 2020 ideas

My Rust 2020 ideas

rust, roadmap

2019-10-31 15:15:00 UTC, by Dimitri Sabadie


This blog article is a small reply to the public call for blog posts 2020 in the Rust community. I will express what I would like Rust to go to, keeping in mind that it’s solely my ideas and opinions others’ might differ.

The points expressed here are written by decreasing priority, starting with the feature I would like the most to see implemented as soon as possible.

Rank-N quantification

It’s for sure one among the two features I miss the most from Haskell. Rank-N quantification is a feature that, when I discovered it almost 9 years ago, changed a lot of things in my way of thinking and desiging interfaces.

For those not used to it or those having no idea what rank-N quantification is, let me explain with simple words by taking an example.

Imagine a function that works on a vector of u32:

fn process(v: Vec<u32>)

That function is monomorphic. You cannot, at compile-time, get several flavours of process. But now imagine you want to process lots of things without actually requiring the element type to be u32 but, let’s say, T: Into<u32>:

fn process<T>(v: Vec<T>) where T: Into<u32>

That function has a type variable, T, and we say that is has a rank of 1, or it’s a rank-1 function. If you add more variables, they all remain at the same level, so the function is still rank-1:

fn process<T, Q>(v: Vec<T>, other: Option<Q>) where T: Into<u32>, Q: Display

Now, imagine a function that would take as sole argument a function which takes a u32 and returns a String, for instance:

fn foo<F>(f: F) where F: Fn(u32) -> String {
  println!("the function returned {}", f(123));
}

That function is still rank-1. But now, imagine that we would like to express the idea that the function that is passed as argument must work for any T: Debug instead of u32. Here, the any word is important, because it means that you must pass a polymorphic function to foo, which will get monomorphized inside the body of foo. If you’ve thought about this:

fn foo<F, T>(f: F) where F: Fn(T) -> String, T: Debug {
  println!("the function returned {}", f(123u32));
}

Then you’re wrong, because that function cannot compile. The reason for this is that the type of f is F: Fn(T) -> String and you try to pass 123u32 instead of T. That function definition cannot work because the body would force T to be u32. The problem here is that, currently, Rust doesn’t allow us to do what we want to: T shouldn’t be monomorphized at the same rank as F, because T will be chosen by the implementation of foo, not the caller!

I would like this:

fn foo<F>(f: F) where F: for<T> Fn(T) -> String where T: Debug;

// or
fn foo<F>(f: F) where F: for<T: Debug> Fn(T) -> String;

We call that a rank-2 function, because it has two levels / ranks of type variables. We could use it this way:

fn foo<F>(f: F) where F: for<T> Fn(T) -> String where T: Debug {
  println!("the function returned {}", f(123u32);
  println!("the function returned {}", f("Hello, world!");
}

You can imagine rank-N quantification by nesting HRTB syntax:

// a rank-3 function
fn foo<F>(f: F) where F: for<T> Fn() -> T where T: for<X> Into<X> where X: Debug;

But it’s rarely needed and I struggle to find a real usecase for them (but there are!). From my Haskell experience, we really really rarely need more than rank-2 quantification.

You can find more in-details thoughts of that feature on a previous article of mine, here.

Kinds

“Kinds” is the second feature I would love to see in Rust. For those who don’t know what they are, consider:

In Haskell but also Idris, ATS, Coq and many others, types have types too. We name those kinds. To understand what it means:

For instance, imagine the kind Number. You can put in that set the types u32, i32, usize, f32, etc. But now imagine: type variables are to types what variables are to values. What would be a kind variable? Well, it would be something that would allow us to give more details about what a type should be. For instance:

trait Functor {
  fn map<A, B, F>(self: Self<A>, f: F) -> Self<B>;
}

impl Functor for Option {
  fn map<A, B, F>(self: Self<A>, f: F) -> Self<B> {
    self.map(f)
  }
}

// whatever the type of functor, just ignore its content and replace with ()
// we could also introduce a type variable A and use fct: Fct<A> but since we don’t
// need it, we use _
//
// The <Fct<_>> marks the kind of Fct (i.e. its kind is Type -> Type, as in it expects
// a type to be type)
fn void<Fct<_>>(fct: Fct<_>) -> Fct<()> where Fct: Functor {
  fct.map(|_| ())
}

Currently, that syntax doesn’t exist and I don’t even know how it would be formalized. The void function above looks okay to me but not the trait definition. The syntax T<_, _> would declare a type which must has two type variables, etc.

GAT

GATs are a bit akin to kinds in the sense that they allow to express type constructors (i.e. which kinds are, for instance, Type -> Type, if they only have one type variable).

That’s a feature I need a lot in several crates of mine, so I hope it will be implemented and stable soon! :)

Polymorphic literals

Something I want a lot too and hasn’t been discussed a lot (I might write an RFC for that because I want it very badly). What it means is that:

let x = 3;

The type of x here would be polymorphic as T: FromLit<usize>. We would have several implementors and it would go like this:

pub trait FromLit<L>: L: Lit {
  const fn from_lit(lit: L) -> Self;
}

// blanket impl
impl<L> FromLit<L> where L: Lit {
  const fn from_lit(lit: L) -> Self {
    lit
  }
}

// generated by rustc
impl Lit for usize {}
impl Lit for isize {}
impl Lit for u32 {}
impl Lit for &'static str {}
// …

This would allow us to do something like that:

pub enum Expr {
  ConstBool(bool),
  ConstI32(i32),
  // …
}

impl FromLit<bool> for Expr {
  const fn from_lit(lit: L) -> Self {
    Expr::ConstBool(lit)
  }
}

// in a function
let expr: Expr = false;

As a rationale, Haskell has that in its base language since forever and under the language extension called OverloadedStrings and OverloadedLists for strings and lists.

Custom operators

A feature that wouldn’t make everyone happy, so I’m pretty sure it will not be in the Rust 2020 roadmap (and maybe never end up in Rust at all, sadly), but I think it’s worth mentioning it.

I would be able to create custom operators. The reason for this is simple: EDSLs. I love EDSLs. Having the possibility to enrich expressiveness via custom operators is something I’ve been wanting for quite a while now and I’m so surprised people haven’t arised that concern yet.

I know there is concerns from people who know the Haskell lens library and its infamous lists of horrible and awful operators, but that’s not a reason to block such a feature to me, for two reasons:

I’m a huge partisan of the idea that there are billions of people speaking Chinese, a language very cryptic to me, because I just cannot speak Chinese. Yet it doesn’t prevent billions of people speaking Chinese on a daily basis without any problem. It’s always a question of learning and an operator should be perceived as a function name. Stop spreading fear about readability: a convoluted function name is also pretty hard to read.

To mitigate fear even further, there are several very good operators in Haskell that are actually very very simple to understand and memorize:

Cargo dependencies

A huge topic, but basically, I hope that cargo can now resolve dependencies without accepting several versions of a crate, but instead resolves them by traversing the whole dependency graph.

This is often needed on my side as I like to make a crate compatible with several dependency versions, so that people who don’t update often can still have their clients benefits from updates on my side. It’s expressed as SemVer ranges (e.g. stuff = ">=0.4, < 0.9") but cargo will take the best one it knows. Basically, if you have such a dependency in project A and you depend on B which has stuff = "0.5", then you will end up with both stuff-0.5 and stuff-0.8 in your graph dependency, which to me is very wrong. Intersecting dependencies should only bring stuff-0.5, because it’s the highest minimal version that satisfies every crates depending on it in the dependency graph.

Sealed traits / private trait items

I talked about it here, but basically, I want to be able to annotate trait’s items with visibility qualifiers (i.e. pub, pub(crate), etc.) so that I can implement a trait in a crate without having people depending on my crate see the guts of the trait.

Sealed traits prevent people from implementing the trait (first new concept) and private trait items both prevent people from implementing the trait but they also prevent them from seeing what’s inside.

Feature discoverability

Long discussion occurring here. Basically, since features are parts of the language (both via cfg attributes and in Cargo.toml), it would neat to be able to show them in rustdoc to help people discover what’s possible to do with a crate, instead of opening the Cargo.toml on GitHub / whatever you’re using.

Conclusion

So that’s all for me and what I would love Rust to go towards. I have no idea whether people from the Rust project will actually read 10% of what I just wrote; I feel like I just made a wish list for Christmas.

Thank you for having read. Thank you for contributing to Rust and making it the best language in the world. And as always, keep the vibes and please let’s talk on Reddit! :)