Home > Enterprise >  How to use only one type parameter to define a generic function in Rust
How to use only one type parameter to define a generic function in Rust

Time:08-12

If functions are traits, only one type parameter should be sufficient to get the function type with arguments and return type.

struct Hello<F>
where
    F: Fn(), //this does not work, it is seen as Input = () and output = ()
{
    output: F::Output,
    f: F,
}

fn hello_test() {
    let f = || 10i32;
    let h = Hello { f, output: 10 }; //I wanted the output to be inferred as i32
}

I could do it with 2 type parameters, but is it possible to do it with just one? Can I get the inputs as well?

CodePudding user response:

No, it is not possible to leave the output type unspecified in the trait bound. Although Fn::Output is defined as an associated type for that trait, the compiler has an explicit safeguard against the standard trait bound syntax because the type parameters of function traits (including also FnOnce and FnMut) are not stabilized.

Attempting to write the trait bound like this:

    F: std::ops::Fn<()>,

Results in this error:

error[E0658]: the precise format of `Fn`-family traits' type parameters is subject to change
 --> src/lib.rs:3:8
  |
3 |     F: std::ops::Fn<()>,
  |        ^^^^^^^^^^^^^^^^ help: use parenthetical notation instead: `Fn() -> ()`
  |
  = note: see issue #29625 <https://github.com/rust-lang/rust/issues/29625> for more information

The function-based syntax for trait bounds also not provide a way to leave the output type unspecified.

So yes, you need two type parameters. But this is very unlikely to be an issue in practice. The compiler will take care of inferring the second type parameter for you.

struct Hello<F, O>
where
    F: Fn() -> O,
{
    output: O,
    f: F,
}

pub fn hello_test() {
    let f = || 10u32;
    let _h = Hello {
        f,
        output: 10, // output type is inferred correctly
    };

    // specifying the return type is also possible
    let _h2: Hello<_, &str> = Hello {
        f: || "ten",
        output: "ten",
    };
}

Playground

CodePudding user response:

There are a couple obstacles to doing this. The first is that direct use of the Fn traits themselves is unstable behind the fn_traits feature, so getting the concrete type of the argument list requires the nightly compiler. For example, the current (unstable!) representation of the arguments for Fn(u32) -> String is (u32,).

That doesn't solve the primary issue, though. The Fn family of traits has Output as an associated type, but the argument list is a generic parameter:

//               vvvv
pub trait FnOnce<Args> {
    type Output;
    extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}

In other words, a type may provide different implementations of Fn depending on the types of the arguments it's called with, and each implementation may have a different output type. Here's an example with two implementations of FnOnce for a single type:

#![feature(fn_traits)]
#![feature(unboxed_closures)]

use std::ops::FnOnce;

struct Foo;

impl FnOnce<(u32,)> for Foo {
    type Output = u32;

    extern "rust-call" fn call_once(self, arg: (u32,)) -> u32 {
        arg.0
    }
}

impl FnOnce<(f32,)> for Foo {
    type Output = f32;

    extern "rust-call" fn call_once(self, arg: (f32,)) -> f32 {
        arg.0
    }
}

fn main() {
    println!("Foo::call(5)   = {:.1}", Foo.call_once((5_u32,)));
    println!("Foo::call(5.0) = {:.1}", Foo.call_once((5.0_f32,)));
}

This outputs:

Foo::call(5)   = 5
Foo::call(5.0) = 5.0

Given that an implementor of FnOnce is (from the trait's perspective) callable with multiple argument list types, and each implementation can output a different type, you must disambiguate the argument list type so the compiler can resolve which trait implementation (and thus which output type) to use.


Existing crates that abstract over functions have typically worked around the instability of the Fn traits by defining their own trait for some subset of Fn implementors, like:

trait CustomFn<Args> {
    type Output;
    fn custom_call(&self, args: Args) -> Self::Output;
}

// Implement CustomFn for all two-argument Fn() implementors
impl<F, A, B, Out> CustomFn<(A, B)> for F
where
    F: Fn(A, B) -> Out
{
    type Output = F::Output;
    fn custom_call(&self, args: (A, B)) -> Self::Output {
        (self)(args.0, args.1)
    }
}

Generally these implementations are macro-generated to avoid having to write implementations for every number of arguments ((), (A,), (A, B), ...).

With our new custom trait, we can write this:

struct Hello<F, A>
where
    F: CustomFn<A>,
{
    // F::Output can be inferred from the argument types
    output: F::Output,
    f: F,
}

fn hello_test() {
    let f = |a: u32, b: u32| a   b;
    let h = Hello { f, output: 10 };
}

And the compiler can correctly infer the output type. (playground link)

  • Related