The Rust code above depends on the programmer to consistently remember to enforce safety, and to do so correctly every time.
Sure, you could probably implement that as a library. But, "We don't see much value in compiler help with this, a combination of libraries and being careful gets the job done," would be a peculiar position for a rustacean to defend.
pub struct Secret<T>(T);
impl<T> Secret<T> {
pub fn map<U>(&self, func: impl FnOnce(&T) -> U) -> Secret<U> {
Secret(func(&self.0))
}
}
impl<T: AsRef<[u8]>> PartialEq<&[u8]> for Secret<T> {
fn eq(&self, other: &&[u8]) -> bool {
constant_time_eq(self.0.as_ref(), other)
}
}
/* Some other file */
use secret::Secret;
// Translated from the example
fn check_mac<T: AsRef<[u8]>>(mac_secret: Secret<T>, message: &[u8], mac: &[u8]) -> bool {
// This returns a new Secret<[u8; 32]>
let computed_mac = mac_secret.map(|secret| hmac_sha_256(secret.as_ref(), message));
// This uses the `constant_time_eq` impl from above
computed_mac == mac
}
I think the interesting part of the example is what you _can't_ do in the other file. It's pretty hard to misuse because the return type of `Secret::map` is a new `Secret`, the only way to do `==` on a `Secret<T>` uses a constant time compare.I guess my main point is that when you have a instead of having to add new things at the language _level_, if I have something as powerful as the rust type system I can implement the same functionality in not much of code.
"need a Monad" sounds scary but in practice it looks like this
impl<T> Secret<T> {
pub fn map<U>(&self, func: impl FnOnce(&T) -> U) -> Secret<U> {
Secret(func(&self.0))
}
pub fn flat_map<U>(&self, func: impl FnOnce(&T) -> Secret<U>) -> Secret<U> {
func(&self.0)
}
}
If you need an escape hatch for something more complicated, you could provide an api to that impl<T> Secret<T> {
pub unsafe fn reveal(&self) -> &T {
&self.0
}
} func(param1: A, param2: B) -> C
with both parameters being secrets (secret1: Secret<A>, secret2: Secret<B>)? In Rust, it‘s flat_map(secret1, |param1|
map(secret2, |param2|
func(param1, param2)
)
)
or with do_notation at least a bit cleaner do! {
param1 <- secret1;
param2 <- secret2;
Secret(func(param1, param2));
}
whereas Rune manages it with func(secret1, secret2)I don't know enough about the subject to really evaluate this in detail, but I am more than willing to at least entertain the notion that the problem space is thorny enough that a language-level solution really can do some things that can't be as effectively accomplished with a library solution. Even in a language with a strong compiler like Rust.
Rune also has an interesting approach to pointer safety that's significantly different from Rust's: https://github.com/google/rune/blob/main/doc/index.md#runes-...
As someone who uses rust, I assume you would prefer the former absolutely.
Edit: mumblemumble helpfully points out I demonstrated this poorly, so I tried to better demostrate what I was going for in this comment https://news.ycombinator.com/item?id=33764037