While having our bounds stated up front in the type system is almost always preferable, sometimes post-monomorphism errors occur. Some of these are ones that would be impractical or even impossible to avoid.
For example, up until recently the bounds that transmute wants could not even be stated in rust. This would be a case of 'impossible'. Even more annoyingly in my opinion, it does not even use post-monomorphism errors, but instead errors on any use of generics at all, even ones that could be trivially checked pre-monomorphism, such as transmuting a reference to a type to a reference to a transparent wrapper around the type.
But as we get more const generic features, I expect it will become increasingly often the case that one merely wants to 'hide' a bound. For example, if one wishes to call two functions which wind up having the const bounds that assert (A + B) + C = X + Y
and A + (B + C) = X + Y
, one probably does not want to have a signature which includes both bounds: after all, either implies the other!
However, in full generality figuring out what bounds are redundant with each other is turing complete, and we probably don't want the compiler to do a fully general proof search that your bounds are sufficient.
A case where limitations of the compiler's reasoning wind up actually creating the problem of 'impossible' occurs with traits. A trait may specify what the strictest bounds a method on it is permitted to have. If the trait says the bounds are (A + B) + C = X + Y
, and we want to call a function that requires A + (B + C) = X + Y
we are sunk (Or are we... more on that later.)
Even without const generics, the same problem can happen by simpler limitations on the compiler's reasoning. If a method has the bound Self: Sized
, and I create a transparent wrapper, forwarding that method is impossible because the compiler will not infer T: Sized
from Wrapper<T>: Sized
. Though I would like to see a transparent newtype wrapper that is sized without its field being sized!
While that last one I think should probably actually be fixed, the const generics case shows that this is rather fundamental.
Therefore, it may be desirable to let the users freely access post-monomorphism asserts and errors. That is to say, make it so a block of code can add extra bounds which are not 'exposed', and are only checked post-monomorphism. I recognize that such things could be easily abused. Hiding a 'debug' bound only to give people unexpected errors when they pass in a non-debug type would be annoying library behavior, to say the least.
However, on nightly this whole system, admittedly with a lot of boilerplate, can be done using unstable features. Including the more dubious uses.
I have been experimenting with it, and wish to release the general form to the community. Both to potentially use on nightly, and to seriously consider if this is actually desired. I think it should be permitted, as many valuable patterns seem to require it, but the tool can easily be abused to basically have ones types simply lie and push us back towards the error situation of C++ templates.
While I kind of want to turn it into a crate with a macro setup, as especially demonstrated by the woggler example it can get complicated, and I am rather uncertain how to make it work in an automated fashion. It is rather similar to the general case of trying to write closures, but trying to reuse the existing closure machinery without any of the intermediate types being invalid requires some extra caution. So while I may work more at trying to wrap it in a nice interface, I encourage those more practiced at such macro and closure manipulations to take their own stabs at it.
trait GivenSizedCallback<R> {
type Target: ?Sized;
fn go(self) -> R where Self::Target: Sized;
}
trait SizedProof<T: ?Sized> {
fn prove<R>(&self, func: impl GivenSizedCallback<R, Target = T>) -> R;
}
impl<T: ?Sized> SizedProof<T> for ! {
fn prove<R>(&self, _: impl GivenSizedCallback<R, Target = T>) -> R {
*self
}
}
impl<T> SizedProof<T> for () {
fn prove<R>(&self, func: impl GivenSizedCallback<R, Target = T>) -> R {
func.go()
}
}
trait GivenDebugCallback<R> {
type Target: ?Sized;
fn go(self) -> R where Self::Target: std::fmt::Debug;
}
trait DebugProof<T: ?Sized> {
fn prove<R>(&self, func: impl GivenDebugCallback<R, Target = T>) -> R;
}
impl<T: ?Sized> DebugProof<T> for ! {
fn prove<R>(&self, _: impl GivenDebugCallback<R, Target = T>) -> R {
*self
}
}
impl<T: ?Sized + std::fmt::Debug> DebugProof<T> for () {
fn prove<R>(&self, func: impl GivenDebugCallback<R, Target = T>) -> R {
func.go()
}
}
trait MaybeConjureProof: Sized {
const RESULT: Option<Self>;
}
impl MaybeConjureProof for ! {
const RESULT: Option<Self> = None;
}
impl MaybeConjureProof for () {
const RESULT: Option<Self> = Some(());
}
const fn try_sized_proof<T: ?Sized>() -> Option<impl SizedProof<T>> {
trait TryProof {
type Result: SizedProof<Self> + MaybeConjureProof + Copy;
}
impl<T: ?Sized> TryProof for T {
default type Result = !;
}
impl<T> TryProof for T {
type Result = ();
}
const fn go<T: ?Sized>() -> Option<<T as TryProof>::Result> {
<<T as TryProof>::Result as MaybeConjureProof>::RESULT
}
const { go::<T>() }
}
const fn try_debug_proof<T: ?Sized>() -> Option<impl DebugProof<T>> {
trait TryProof {
type Result: DebugProof<Self> + MaybeConjureProof + Copy;
}
impl<T: ?Sized> TryProof for T {
default type Result = !;
}
impl<T: ?Sized + std::fmt::Debug> TryProof for T {
type Result = ();
}
const fn go<T: ?Sized>() -> Option<<T as TryProof>::Result> {
<<T as TryProof>::Result as MaybeConjureProof>::RESULT
}
const { go::<T>() }
}
fn debug_format_given_proof<T: ?Sized>(proof: impl DebugProof<T>, value: &T) -> String {
struct Worker<T>(T);
impl<'a, T: ?Sized> GivenDebugCallback<String> for Worker<&'a T> {
type Target = T;
fn go(self) -> String where T: std::fmt::Debug
{
format!("{:?}", self.0)
}
}
proof.prove(Worker(value))
}
// We can even branch on it in the current nightly
fn try_debug_format<T: ?Sized>(value: &T) -> Option<String> {
try { debug_format_given_proof(try_debug_proof()?, value) }
}
// Enabling tricks like this
fn debug_with_fallback<T: ?Sized>(value: &T) -> std::borrow::Cow<'static, str> {
match try_debug_format(value) {
None => std::borrow::Cow::Borrowed("{NO DEBUG}"),
Some(v) => std::borrow::Cow::Owned(v),
}
}
fn call_only_with_debug<T: ?Sized>(value: &T) -> String {
let debug_proof = const { try_debug_proof::<T>().expect("Only call with debug types") };
debug_format_given_proof(debug_proof, value)
}
trait Wobbler {
fn widdle(&self) -> usize;
fn woggle(self) -> usize where Self: Sized; //Wants to be dyn safe!
}
struct Wrapper<T: ?Sized>(T);
impl<T: ?Sized + Wobbler> Wobbler for Wrapper<T> {
fn widdle(&self) -> usize {
self.0.widdle() + 1
}
// This doesn't work
/*
fn woggle(self) -> usize where Self:Sized {
self.0.woggle() + 1
}
*/
// Nor this
/*
fn woggle(self) -> usize where T:Sized {
self.0.woggle() + 1
}
*/
// This works, and I believe may be impossible without this trick
fn woggle(self) -> usize where Self: Sized,
{
struct Woggler<T>(T);
trait WogglerHelper {
type Target: ?Sized;
fn woggler_helper(value: Woggler<Self>) -> usize where Self: Sized, Self::Target: Sized;
}
impl<T: ?Sized + Wobbler> WogglerHelper for Wrapper<T> {
type Target = T;
fn woggler_helper(value: Woggler<Wrapper<T>>) -> usize where T: Sized
{
value.0.0.woggle()
}
}
impl<T: WogglerHelper> GivenSizedCallback<usize> for Woggler<T> {
type Target = T::Target;
fn go(self) -> usize where T::Target: Sized
{
T::woggler_helper(self)
}
}
let proof = const { try_sized_proof::<T>().expect("How is this type sized but its contents aren't???") };
proof.prove(Woggler(self)) + 1
}
}
fn main() {
struct NotDebug;
println!(
"Look ma, no exposed type constraint: {}",
&*debug_with_fallback(&5)
);
println!("Here we fallback: {}", &*debug_with_fallback(&NotDebug));
//println!("If this weren't commented it would fail: {}",call_only_with_debug(&NotDebug));
println!("This works: {}", call_only_with_debug(&6));
struct Wob(usize);
impl Wobbler for Wob {
fn widdle(&self) -> usize {
self.0
}
fn woggle(self) -> usize {
self.0
}
}
println!("Here we use the wobbling: {}", Wob(7).widdle());
println!("Here we use the wrapper: {}", Wrapper(Wob(8)).widdle());
println!("Woggling works too on the wob: {}", Wob(9).woggle());
println!("And the wrapper: {}", Wrapper(Wob(10)).woggle());
let dyn_demo: Box<dyn Wobbler> = Box::new(Wob(11));
println!("And we can use dyn Wobblers: {}", dyn_demo.widdle());
}