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
- Kinds
- GAT
- Polymorphic literals
- Custom operators
- Cargo dependencies
- Sealed traits / private trait items
- Feature discoverability
- Conclusion
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:
- A value is something that lives at runtime. In Rust, it has a type. You can refer to values
directly with literals or you can bind them via let bindings, for instance. In
let x = 3
,3
is a value andx
is a binding to that value. - A type is like a value that lives at compile-time. You can refer to types directly or via
type variables. For instance,
u32
is a type and infn foo<T>()
,T
is a type variable. It’s actually a free variable, here. Currently, Rust stops here.
In Haskell but also Idris, ATS, Coq and many others, types have types too. We name those kinds. To understand what it means:
- A type is just a label on a set.
u32
is a label (really, imagine"u32"
) of a very big set that contains all the possible values that can be labelled asu32
. You find in that set0
,1
,34
,2390743
, etc. - A kind is just a label on a set, too. But that set doesn’t contain values; it contains types. A kind is just a labelled set of types.
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:
lens
is really extreme is likely the sole real problem in the Haskell ecosystem.- We could mitigate that fear by forcing custom operators to have a function associated with the operator, so that people who don’t want to use the custom operator can still use a correctly named function.
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:
- The
fmap
function operator version is<$>
. - When applying
fmap
to ignore what’s inside a functor and replacing it with a constant, we usefmap (const 32)
, for instance — orfmap (\_ -> 32)
for the lambda version. You can use<$>
too,const 32 <$> [1, 2, 3]
. But there’s a very logical and simple operator to remember. Notice how the constant value is at the left of the<$>
operator? Then, you can do the exact same thing with32 <$ [1, 2, 3]
. On the same level, if you prefer to put the value on the right side:[1, 2, 3] $> 32
. Simple. - The
<|>
operator. It has that|
in it, which is oftenOR
in most languages. And guess what: that operator is the alternative operator.a <|> b
equalsa
if it’s true orb
if not. The definition of truth is up to the type, but forMaybe a
—Option<T>
in Rust, true isJust _
(Some(_)
) and false isNothing
(None
). See how easy it is? - In parsec, one of the most famous Haskell parser, there’s
an operator you can use to provide more descriptive error messages for all parsers (even sub-parsers!):
the
<?>
operator. Once again, the?
reminds a question, information, etc. - The
.
operator in Haskell composes two functions. It looks closely to the∘
math notation. - The monadic bind is
>>=
, which expects the function on the right hand side… Guess what the=<<
operator does. The exact same thing, but expects the function to be on the left hand side. - When dealing with paths, the
</>
operator allows to separate path parts without redundant/
. The<.>
operator allows you to write the extension name!"/home/phaazon" </> spareProjectsDir </> "rust/rfcs" </> lastRFC <.> "md"
. - Etc. etc.
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! :)