Tracking Issue for ops::Residual
(feature try_trait_v2_residual
) · Issue #91285 · rust-lang/rust
Yeah, the current Residual
trait reads rather awkwardly due to that generic O
parameter it must be fed eagerly:
fn try_from_fn<R, const N: usize, F>(cb: F) -> ChangeOutputType<R, [R::Output; N]> where F : FnMut(usize) -> R, R : Try, R::Residual : Residual<[R::Output; N]>,
Notice that : Residual<[R::Output; N]>
clause. It can't really be spelled out/read out loud in English: "is a residual of an array / can residual an array" doesn't make that much sense.
In a way, the type alias internally used by the stdlib already provides a way nicer name: ChangeOutputType
.
So in this case, taking that name and using it on the trait itself, we would rather imagine:
R::Residual : CanWrapOutputType<[R::Output; N]>,
- (or
{Feed,Apply,Change,Set,With}OutputType
)
Now we get something that does read better, although it talks a bit too much about type-level operations, which is something the stdlib doesn't do that often.
So, rather than focusing on the how, if we focus on the what instead, we can stick to the "being a Residual
" property, but this time with no generic Output
parameter yet, by delegating it to a GAT, which features the proper quantification of "it can be fed any output type":
R::Residual : Residual, // <- Bonus: this wouldn't even be needed, since now we could eagerly add this bound to `Try`!
and then using <R::Residual as Residual>::TryType<[T; N]>
.
- Or even
TryTypeWithOutput<[T; N]>
. I'll be using this new name for the remainder of the post
Notice that bonus of being able to eagerly require that Try::Residual
types always implement Residual
, which we can't do with the current design since that would need a for<Output>
kind of quantification. EDIT: not all Residual
s / Try
types may want to be able to wrap any kind of T
, as pointed out by @h4x5 in the comment just below.
I think it would be confusing to have some Try
types not be usable with array_from_fn
just because of the Residual
associated type happens not to meet part of the implicitly required API contract. Having it explicitly required seems like a definite win, in that regard.
A technical remark, however, @h4x5: we can't use Res : Residual
and then an impl Fn… -> Res::TryTypeWithOutput<T>
closure, and then expect Res
to be inferrable from context. Indeed, we'd have a "preïmage" situation regarding the type FeedT<Res> = Res::TryType<T>;
operation, which is not an injective one, and thus won't be solvable by type inference.
So, while keeping Res
seems convenient for the sake of the signature (Res::TryType<…>
), we'd have to instead say that the output of the closure implements Try<Residual = R>
:
fn try_from_fn<T, const N: usize, F, Ret, Res>(f: F) -> Res::TryTypeWithOutput<[T; N]> where F : FnMut(usize) -> Ret, Ret : Try<Output = T, Residual = Res>, Res : Residual, // EDIT
- Notice how now we are getting the associated "image"/type from
Ret
toRet::Residual
in order to figure outRes
.
Or we could get rid of that Res
altogether:
fn try_from_fn<T, const N: usize, R, F>(f: F) // -> <R::Residual as Residual>::TryTypeWithOutput<[T; N]> -> ChangeOutputType<R, [T; N]> where F : FnMut(usize) -> R, R : Try<Output = T>, R::Residual : Residual, // EDIT