Omit functionally dependent generic parameters

Is there a (good) way to omit "generic parameters" that are uniquely determined by other generic parameters (or Self)?

Consider the following code:

fn first_ok_1<I, O, E>(iter: I) -> Option<O> where I: IntoIterator<Item = Result<O, E>> {
    iter.into_iter().filter_map(Result::ok).next()
}

Here O and E are uniquely determined by I, so the function is not truly generic over them. Yes they can (almost?) always be inferred, but as user facing API it does not look nice (especially for E which we do not care about and it is not mentioned anywhere in function signature). Also if the generic item were a struct instead of function, these parameters might become infectious.

Overall it feels like being forced to write function(s) like this:

fn compute_something(value: i32, square: i32) -> i32 {
    assert_eq!(square, value * value);
    // rest of computation
}

One way around that I found is to introduce an extra trait to turn them into associated types:

trait IntoIterResult: IntoIterator<Item = Result<Self::Good, Self::Bad>> {
    type Good;
    type Bad;
}

impl<I, O, E> IntoIterResult for I where I: IntoIterator<Item = Result<O, E>> {
    type Good = O;
    type Bad = E;
}

fn first_ok_2<I>(iter: I) -> Option<I::Good> where I: IntoIterResult {
    iter.into_iter().filter_map(Result::ok).next()
}

But that requires to expose this helper trait in public API, which also does not look nice.

Is there a better way?

1 Like

in the function signature, you have to be able to name the output type, either as a generic type parameter, or as an associated type.

if you think neither of the implementation is acceptable, then probably it's a design problem and you might want to rethink the overall design.

it's hard to say what's better or worse without more context, since the example is simplified.

but, just for the one-liner in the example, I would consider it unnecessary to create a wrapper: the user can just do the iterator-chain directly.

1 Like

They have to be listed, however, because you're telling the programmer that the types O and E are chosen by the caller, not by the function defined here.

That is, while the type I choose for I does determine what O and E are, the type I choose for I has freedom to choose O and E (but, importantly, it's not free to choose an arbitrary Item as associated type - Item has to be Result<O, E>).

Somewhere has to determine what O and E are, and the way you tell Rust that it's the caller who chooses them is to put them in the <_> list as generic parameters.

Yes, but here it is a parameter of associated type (which happens to be an instance of generic type) ... which mostly behaves like associated type, but is not expressible like one.

well ... they are not unacceptable, let's just say suboptimal.

btw. I have found another (maybe practically unusable) possibility:

struct FirstOk3<I>(PhantomData<I>);

impl<I, O, E> FirstOk3<I> where I: IntoIterator<Item = Result<O, E>> {
    fn call(iter: I) -> Option<O> {
        iter.into_iter().filter_map(Result::ok).next()
    }
}

overall I meant my question more like "whether there is an option I missed".

I can easily imagine syntax like (refer to generic parameters as if they were associated types):

// If such "structural bounds" and "parameter assoc. type" were possible
fn first_ok<I>(iter: I) -> Option<<I::Item as Result>::Arg0> where I: IntoIterator<Item = Result<_, _> { ... }

or (introduce extra variables into scope without them being generic parameters of the function):

// If "free" `impl` blocks were possible
// As is usual for impl blocks, if I, O, E were not uniquely determined
// by I, this would raise the "unconstrained parameter" error
impl<I, O, E> {
  fn first_ok<I>(iter: I) -> Option<O> where I: IntoIterator<Item = Result<O, E>> { ... }
}

I agree, the iterator chain was just an example to have something concrete and familiar.

Well ... the caller chooses I. Types O and E then follow from that. But there is no(?) syntax to say "the unique O and E that solve the given bounds". There is such syntax for impl blocks[1], but not for functions (nor other items).


  1. as I demonstrated above: there is no choice of O and E when invoking FirstOk3::<_>::call(something) ↩ī¸Ž

Types O and E are still chosen by the caller; the caller is not given free reign to choose any type for I, or even any arbitrary I: IntoIterator. For example, if I as caller choose I to be Vec<u32>, which is an I that implements IntoIterator<u32>, it's not going to work; you've constrained the type of the Item to Result<O, E>, where I choose O and E.

well, it is expressible: define a helper trait, which is how your first_ok_2 is implemented.

in fact, if you see Result as a trait (instead of a type), this can be valid rust; but you must provide the trait yourself, there's no such trait in the standard library.

trait ResultHelper {
    type Output;
    type Error;
    // need some form of trait method,
    // because implementation of `first_ok()` only knows this trait,
    // it doesn't know the `Result<T, E>` type
    fn ok(self) -> Option<Self::Output>;
}
impl<T, E> ResultHelper for Result<T, E> {
    type Output = T;
    type Error = E;
    fn ok(self) -> Option<T> { ... }
}

fn first_ok<I>(iter: I) -> Option<<I::Item as ResultHelper>::Output>
where
    I: IntoIterator,
    I::Item: ResultHelper,
{
    iter.into_iter().map_filter(ResultHelper::ok).next()
}

as you can see, if you don't want to name the type as generic parameters, you need to emulate "a function on types (that takes a type and returns another type)", you must use a trait and its associated types, in one way or another.

you can define this trait for the generic type I: IntoIterator, or you define it for type Result<T, E>, or any solutions you can came up with, but a trait is unavoidable.

3 Likes

If the function definition is like fn first_ok<I, O, E>(...) -> ... {...} then yes, the caller chooses O and E. But it does not (in my opinion) make good API to give you choice and simultaneously restrict it to at most one option.

So I am asking whether there is a way to let/make the caller to choose only I (i.e. define the function like fn first_ok<I>(...) -> ... {...}) and "calculate" O and E by myself.

Yes. Remove the O and E from <Item = Result<O, E>>, since you're saying that you don't want the caller to choose O or E, and replace them with concrete types. That will allow the caller to only choose I, but not O or E.

I: IntoIterator<Result<O, E>> is not a complete type constraint without telling us what types are chosen for O and E; you can either let the caller choose them (in which case, they need to be listed as types the caller chooses), or you can select them on behalf of the caller, in which case you can fill them in, and they're forced by the choice of I.

With the example you listed, though, I have more than one choice of I, O and E; I can choose I to be Vec<Result<_, _>>, but that doesn't force a single choice of O and E. The reason you list O and E is that you want the caller to choose them.

I think we are in a big misunderstanding about what does it mean "to choose". I would say you cannot choose I = Vec<Result<_, _>>, because it is not a complete type. You have to fill in the placeholders. And once you do that, there is only one "choice" for O and E that solves the bounds. So the choice is only formal.

However ... were you talking about something like this?

let _ = first_ok_1<_, i32, String>(vec![]);
// or
let _: Option<i32> = first_ok_1<_, _, String>(vec![]);

I would not describe that as "caller chose I = Vec<Result<_, _>>, O = i32, E = String". I would say "caller chose I = Vec<Result<i32, String>>, O = i32, E = String". The fact that I was inferred and not explicitly spelled out in the code does not change that.

I think that "who chooses what" and "how the type inference goes" are independent questions.

But it is true that the redundant parameters of first_ok_1 provide good place for type annotations if they are needed (even without requiring user to mention Vec or Result) so maybe having them in API is not as bad as I originally thought.

2 Likes

A non-answer tangent to your question: You can use the nightly Try trait to get away without an additional trait. Rust Playground

1 Like

If you don't give me type parameters for O and E, then I can't fill in the placeholders, because I have nothing I can legally fill them in with - my only caller-chosen type in fn foo<I> is I, and that would be an infinite recursion. You give me O and E to give me permission to fill in those placeholders with any type I choose that meets the constraints you place on them; as you don't place any constraints on them, I can choose any type for O or E.

Unless you're saying that having chosen to use impl<O, E> IntoIterator<Item = Result<O, E>> for Vec<Result<O, E>> as the fulfilment of the constraint I: IntoIterator<Item = Result<O, E>>, I can't freely choose O and E because they're fixed, which to my eyes is nonsense - it would imply that I can't choose between Vec<Result<u32, String>> and Vec<Result<String, usize>>, since you're saying that O is fixed by the fact that I've chosen impl<O, E> IntoIterator<Item = Result<O, E>> for Vec<Result<O, E>>.

And you need to explicitly name the types I can choose between, since your example is identical bar naming of type parameters to:

fn first_ok_1<I, Output, Error>(iter: I) -> Option<Output>
where
    I: IntoIterator<Item = Result<Output, Error>>,
{
    iter.into_iter().filter_map(Result::ok).next()
}

If you have use std::process::Output; use std::io::Error; at the top of the module with this in, how is Rust supposed to know that you mean to have Output and Error be type parameters, and not the concrete types you've imported? By naming the type parameters, you're explicit - "here, Output and Error are type parameters, shadowing the imported names".

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.