To answer the thread's title, rather than the XY problem that lead to it (go for @skysch suggestion there), for other people stumbling upon this question:
Rust doesn't really have SFINAE, except in the method resolution and assoc item resolution algorithm, where it allows shadowing:
-
inherent assoc item can shadow a trait-provided one;
-
a .method()
which does not require dot-auto{,de}ref can shadow one that does.
So, if you feature a shadowing thing which is only available based on implementing a trait / or some other trait guards —except for lifetime-based ones—, and then have a potentially-shadowed thing which is unguarded, you'll effectively have a form of SFINAE w.r.t. the trait bounds: if they're met, it uses the shadowing assoc item, else it uses the other one, which kind of acts as a fallback (this is the key aspect of SFINAE, as I view it, and this is the "common" aspect in Rust).
-
If using the .method()
approach, this leads to:
-
Autoref-based specialization, if using .
-dot operator auto-ref;
-
Autoderef-based specialization, a generalized variant of the previous ones (allows to easily feature multiple "SFINAE layers"), based off, you guessed it, .
-dot operator auto-deref.
The only drawback of these approaches is that they require trait-based methods, which currently cannot be const
in stable Rust, which thus make it incompatible with const
ants, and, from there, unable to be used for type resolution.
-
Using the inherent vs. trait
-provided assoc item (fn
or const
) approach, one can then thus feature "SFINAE" const
ants, and with them, using a const-generic type, one can eventually reach "SFINAE" types.
Note that any of these "SFINAE" approaches are:
-
very boilerplate-y,
-
unable to properly work with generic types (if you have a fallback for a !Sized
case, say, then given <T : ?Sized>
, the fallback path will be taken, for all T
s, even if later on the specific choice of T
happened to be Sized
. The same applies for the other traits.
Both of these things make these "Rust SFINAE hacks" be only truly suitable for macros, which can hide the boilerplate, and which can handle any caller-provided type (a form of (meta-)polymorphism), without involving generics.
Example
Here is an example of the inherent-assoc-based approach: we'd like to feature a wait!
macro that shall be given a Future
—or not!— and it shall .await
it if it is one, or do nothing if it isn't.
- In practice, this is implemented as a conversion to a future that is unconditionally
.await
ed, where that future is value
if it is a future already, or async { value }
otherwise.
The semantics could then be viewed as async { value }.flatten().await
, where the .flatten()
is the true SFINAE-based implementation.
#[doc(hidden)] /** Not part of the public API */ pub
mod __ {
// To get an inherent method for _any_ type we use a wrapper type.
pub struct Wrapper<T>(pub T);
// SFINAE attempt: inherent assoc items
impl<T> Wrapper<T>
where
T : ::core::future::Future, // the "fallible guard"
{
pub
fn __into_fut_flattened (self: Self)
-> T // impl Future<Output = T::Output>
{
self.0
}
}
#[extension(pub trait Fallback)] // From `::ext-trait`
impl<T> Wrapper<T> {
fn __into_fut_flattened (self: Self)
-> ::futures::future::Ready<T> // impl Future<Output = T>
{
::futures::future::ready(self.0)
}
}
}
and with it, the macro:
#[macro_export]
macro_rules! wait {( $e:expr $(,)? ) => (
match $crate::__::Wrapper($e) { wrapper => {
#[allow(unused_imports)]
use $crate::__::Fallback as _;
wrapper.__into_fut_flattened()
}}.await
)}
Demo
#[::tokio::main]
async
fn main ()
{
assert_eq!(42, wait!(42));
assert_eq!(42, wait!(async { 42 }));
}