Silly idea, but I wish for a new keyword like danger, which is similar to unsafe where danger functions can only be called in danger blocks, though it doesn't enable any new unsafe operations.
I want it because quite often I want to add "dangerous" functions to my APIs, like converting an int to a certain type, and I don't want to use unsafe there, since it implies possible undefined behavior in case of misuse, which is not the case for my usage.
But I do want to explicitly state the intent and make linters warn about missing SAFETY docs.
As a silly example:
struct Even(u64);
impl Even {
// Is just a simple function
pub fn get_unchecked(n: u64) -> Self { Self(n) }
// Implies too much
pub unsafe fn get_unchecked_2(n: u64) -> Self { Self(n) }
// Forces me to write `danger` at usage and linters warn about missing safety docs
/// # Safety
/// 1. `n` must be even.
pub danger fn get_unchecked_3(n: u64) -> Self { Self(n) }
pub fn get(n: u64) -> Option<Self> {
match n % 2 == 0 {
// SAFETY:
// 1. Check above verified the condition
true => Some(danger { Self::get_unchecked_3(n) })
false => None,
}
}
}
This is exactly what unsafe is used to. Your struct Even is a wrapper around u64 with the invariant that the inner value is even. Constructing an Even struct with an odd value can (and probably will) cause undefined behavior, usually because other functions will assume that the inner value is even. Therefore, danger is the same as unsafe. You can activate the lint unsafe_op_in_unsafe_fn as deny or forbid, denying any call to unsafe code in an unsafe function without using another unsafe block. This way all unsafe functions behave like your danger, and can only use unsafe code in the same way every other function does (note that normal functions can use unsafe, and can do it wrong too. without checking source code you don't know if something will cause UB).
So, your code becomes:
/// An even number.
///
/// # Invariants
/// * inner value is divisible by 2.
#[repr(transparent)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default)]
pub struct Even(u64);
impl Even {
/// Creates an [`Even`] number, or returns `None` if it's odd.
#[inline]
pub fn new(n: u64) -> Option<Self> {
// or also `n & 1 == 0`
if n % 2 == 0 { Some(Self(n))} else { None }
}
/// Creates an [`Even`] number without checking if it's even.
///
/// Passing an odd number to this function will cause *Undefined Behavior*.
///
/// # Safety
/// `n` must be even (i.e. `n % 2 == 0`)
#[inline]
pub unsafe fn new_unchecked(n: u64) -> Self {
Self(n)
}
#[inline]
pub fn get(self) -> u64 { self.0 }
// example of a function that will be wrong if the safety conditions are violated
// (this being part of a trait could make more sense, but this is just an example):
#[inline]
pub fn is_prime(self) -> bool {
// note: an even number is divisible by 2 and therefore not prime.
// Only `2` itself is both even and prime.
self.get() == 2
}
// another method could use
// unsafe {
// std::hint::assert_unchecked(self.get() % 2 == 0)
// }
// and thus cause UB, and not just a wrong result
}
Constructing an Even struct with an odd value can (and probably will) cause undefined behavior
Undefined behavior has very specific definition, and this is just isn't it.
And I would argue unsafe exists solely for UB, it just turned out it has nice properties, which I would want to have for my own specific domains as well.
This is not just me who thought about it. Bevy used to have unsafe methods, which became safe and got _unchecked suffixes to them. I would argue that my danger keyword would suit exactly for that case.
Jon Gjengset pointed out something similar in his talk on type safe math, though he decided to use unsafe anyway: https://youtu.be/kESBAiTYMoQ
The more you want to do in generic code, the more trait bounds you must add. And thatâs the happy path. Very many (e.g. std-lib) methods are totally not accessible, because they come from non-trait direct impls. So instead of generics, you must put that code in a macro, where it will easily infer type/method combinations.
But that opens the next can of worms: Much as I love pattern macros, they have many downsides. They canât handle valid Rust, e.g. (omiting vars) if :expr :block is not allowed, because :expr is more limited than actual Rust exprs. Even less the new chains, which are a funny mix of syntax and logics, &¬ being part of the expr. This would require an often desirable non-greedy match: if $(let :pat = :expr &&)*? :expr :block (ambiguous, as *? might occur today â so instead maybe if $?(let :pat = :expr &&)* :expr :block.)
Also thereâs no matcher for traits. Fine, :ty will do nicely â except⌠not: Youâre not allowed to generate impl :ty for :ty {}, as for is not allowed after a :ty!
While Rust is type safe, good luck disambiguitating :literal! It would also need :char, :str, :bool, and :num, which could be further refined as :int & :float.
Edit: I accidentally had the && after the group. Putting it inside requires something like if $(let :pat = $?(:expr) &&)* :expr :block.
Didn't say that that function was immediate UB. It can cause UB later on. Look at NonZero::new_unchecked and the whole structure of NonZero in general. It's the same as your struct Even.
Compiler relies on the fact that NonZero cannot be zero for niche optimization. And constructing NonZero with zero is immediate UB.
Proof (run Tools > Miri): Rust Playground
While I understand the fact that UB can be non-immediate with unsafe, Vec::set_len, for example. Making Even odd inside will only result in bugs in the program, it will not make Rust program incorrect like actual UB will.
Again, check the definition of UB here:
Libraries can defined their own soundness requirements (and don't necessarily have the tools to make their invariants instant UB like std does). So if Even performed some unsafe based on the integer being even, constructing a non-even one would be one of those library UB situations.
Which I recognize is not what you're talking about; you just want a "could cause logic bugs if you're not careful" marker or such, and were'n't planning on performing unsafe.
As quinedot said (I didn't find the source so I didn't talk about this point before)
and also having an odd number inside Even can obviously cause UB. The first thing that comes in my mind is just another method calling assert_unchecked(even.get() % 2 == 0) which will cause instantly UB, but there's also many other cases of this happening. If a livrary says something is UB, you know that in any method there could be a call to assert_unchecked like seen before or any other thing that causes UB based on the struct invariants, and even if that never happens, you may assume that functions implementation could later change making that happen, or that new functions that are not yet implemented may make that happen in the future.
You are focusing too much on the silly example that I showed. It is here just to show the point of some high-level invariants in the program not warranting use of unsafe. The proper one would be something like SQL validation, where, if you are wrong, you can get SQL injection, but the program is still well defined. I don't think unsafe is proper there, since it gives me way more than I want. While not having a marker also feels wrong. (_unchecked suffix is sad)
Note that both of these may fail during the instantiation phase which makes language designers very reluctant to support them. I, for one, hate that choice, but I can understand where they come from.
Not when they are simply used as templates and just need to sprinkle your code with some template-provided variables. They work fine for that.
Why do you want to provide valid Rust as input to your macro?
My trait/impl case was where I failed with exactly that. What I usually do when I canât match what I want to is [$(:tt)+], where the brackets limit how many tokens this gobbles.
Exactly because of the above, where it takes random workarounds, i.e. the macro defines a whole new language. Much easier when the input is as close to known syntax as possible!
All the more so, as they are thinking about enabling pattern attribute macros, which will have to parse real Rust.
in my opinion every type should be allowed to safely operate under the assumption that it's state corresponds to the invariant it's supposed to have and breaking said invariant should be assumed as a potential cause of UB so if you delegate to the caller to respect and invariant it is an unsafe function.
after all why should any implementation be required to check that unmodified state coming from itself is respecting it's own invariants and what about users of the type why would anyone writing unsafe code be asked to check all the internal invariants of safe surces
especially because if you are not checking invariants then you almost certainly are in perfomance critical scenario where you are very much likely to rely on that invariant for safety anyway
in my opinion every type should be allowed to safely operate under the assumption that it's state corresponds to the invariant it's supposed to have
I totally agree on that part and that's how I usually write code also.
breaking said invariant should be assumed as a potential cause of UB
However, I don't agree with that. Because there is a clear distinction in my head between the actual UB that can be caused by unsafe usage and a generic bug in the program.
In general, I don't want to imply usage of UB causing things when I don't use them.
TBH, I suggest just putting the word dangerous or insecure or similar in the name of the function.
httpClient.insecure_ignore_certificate_validation() is fine; it doesn't need a danger block.
(Basically, the core problem is that everything might be "dangerous", in some sense. Does deleting a file need a "danger" block, for example? What about sending stuff to an HTTP server? Certainly those things can be used in dangerous, malware-like ways. Even addition can be "dangerous" if you're in a place where release-mode-wrapping isn't what you wanted. The reason unsafe works is because it can hold a very hard line about UB -- the best APIs make that as strict as possible too, like how MaybeUninit makes it safe to create uninitialized memory and write to uninitialized memory, with the only unsafe being when you claim it's not uninitialized any more.)
It's quite common to wish that at first; I did miss inheritance and better encapsulation when I started programming in Rust. Now I'm not sure it's quite so necessary, though I'm still missing the ability to have non-public method in trait implementations; hiding them artificially as general functions feels odd, but thankfully I haven't had to do that often.
Inheritance would make UI design easier, for sure, but my feeling is that it'd make a weird combination with traits. There are more pressing matters than a new paradigm anyway.
There are also number of very vocal opponents to inheritance, who are often misquoting Design Patterns, by the Gang of Four (IIRC, you can find an example of that in the "Rust Book"). It has its usefulness, but Rust provides enough tools so that it's not too painful.
I also doubt it'd make Rust more successful than it already is. The strong points of Rust are safety and performance, but it comes with a steep learning curve and the borrow checker. OOP wouldn't change those main pros/cons.
Progress on const expr functionality, min_const_exprs is useful but rather limited
Significant progress on the librarification effort, because ATM it feels like the Rust compiler wheel has been invented (and require continual maintenance) at least 3 separate times: rustc, rust-analyzer and rust-gcc, which means a whopping 200% overhead every time a feature lands that has a significant syntactic surface. It would be great to see those efforts streamlined and (sigh...synergize) more, and reduce the total amount of effort it takes to get a feature supported everywhere. In addition to that, it would be nice to eg be able to call the Rust solver from eg macros, which would likely enable new code gen patterns (at the cost of running the analyzer an additional time, while the macro runs).