-
Notifications
You must be signed in to change notification settings - Fork 2
The Colorful Rust
In languages like JavaScript, "what color is your function" refers to a simple black and white: is it sync or async? People make a big deal out of this topic and brought about concepts such as fibers.
Nonetheless, sync and async are not the only colors we have.
In OOP languages, setters, fluent setters and non-mutating (cloning) setters are a dimension of colors;
in Go, whether a function may propagate errors is another dimension.
But few are as colorful as Rust.
In this article, I will go through a few colors that have caused true annoyance to me,
as well as why... well, why I still love our colorful and inclusive language.
Yep, continue with the old story first.
The story of async is actually a bit better in Rust.
Thanks to the #[must_use] attribute,
it is easier to spot this bug as you would have in JavaScript:
async function flush() {
for(const message of buffer) {
await send(message)
}
}
flush()
process.exit(0) // we stopped the process before it finished flushingIn Go, developers constantly struggle whether to make an interface
return Value or (Value, error)
(let's not get started on how terrible Go interfaces are without type classes).
Rust brings this even further —
instead of an error type, each crate (or module, depending on the scenario)
declares their own error types, which the developer has to manually convert for every case.
This means every
Mutability is probably one of the worst problems with ergonomics in Rust.
While mutable references (&mut T) are a subtype of shared references (&T),
they are often used in invariant context:
fn foo(input: &Input) -> &Output;Since inputs are contravariant and outputs are covariant, to get a mutable output, there is no way but to create a separate function:
fn foo_mut(input: &mut Input) -> &mut Output;This pattern is common in types that expose getters, such as [T]::get/[T]::get_mut,
as well as traits such as Fn/FnMut, Index/IndexMut, Deref/DerefMut, etc.
This problem is explored in RFC #414, which is among the oldest RFCs from 2014. Unfortunately it seems little attention has been paid to study this.
Wait... now that we mentioned Fn and FnMut, how could we forget FnOnce?
In fact, there is also an RFC
regarding IndexMove and DerefMove as well.
Rust has a very rigorous notion of safety:
unsafe should be added to a function or a trait if and only if
removing the unsafe allows users to trigger undefined behavior in some way
(even in obviously mischievous ways).
As a result, safety is typically very well-encapsulated to minimize the vulnerability surface.
However, there are some cases where safety is an infectious color. For example, consider a simple map-splitting function:
fn extract_multi(map: &mut HashMap<usize, Foo>, a: usize, b: usize) -> (Option<&mut Foo>, Option<&mut Foo>) {
assert_ne!(a, b, "Cannot alias the same key);
unsafe {
(
map.get_mut(&a).map(|foo| &mut *(foo as *mut Foo)),
map.get_mut(&b).map(|foo| &mut *(foo as *mut Foo)),
)
}
}This function is sound because a and b map to different values in the HashMap.
What if we try to generalize usize into our own trait, let's say, MapKey?
trait MapKey : Eq + Hash {
fn to_usize(&self) -> usize;
}
fn extract_multi<K: MapKey>(map: &mut HashMap<K, Foo>, a: K, b: K) -> (Option<&mut Foo>, Option<&mut Foo>) {
assert_ne!(a, b, "Cannot alias the same key);
unsafe {
(
map.get_mut(&a).map(|foo| &mut *(foo as *mut Foo)),
map.get_mut(&b).map(|foo| &mut *(foo as *mut Foo)),
)
}
}This is unsound because inequality of a and b does not necessarily imply a and b cannot alias.
For example, if someone implements Eq for a wrapper for f64,
the function would return an aliased value for extract_multi(map, NAN, NAN) because !(NAN == NAN) is true.
To maake sure Eq is implemented properly, we need to make MapKey an unsafe trait.
This means all implementors need to add an unsafe marker,
just because we are worried someone may pass two NANs into extract_multi...
In the end of the day, colors are just combos of more or less common use cases. Wherever programming is needed, new combos always exist. I will never forget that day when I found example code for async WSS client, async WS server, async HTTPS server and sync WSS server, but just no async WSS server...