rusttypes

How to write a constructor in Rust that accepts a simpler closure and infers the full generic type?


I have an object that takes a callback function. The callback function takes two arguments.

I want to create a convenience constructor that allows passing in a simpler callback, that takes only one argument. But I can't get it to compile.

Code:

use std::fmt::Debug;

struct Thing<A, B, F>
where
    A: Debug,
    B: Debug,
    F: Fn(A, B) -> (A, B),
{
    f: F,
    _dummya: std::marker::PhantomData<A>,
    _dummyb: std::marker::PhantomData<B>,
}

impl<A, B, F> Thing<A, B, F>
where
    A: Debug,
    B: Debug,
    F: Fn(A, B) -> (A, B),
{
    fn new(f: F) -> Self {
        Self {
            f,
            _dummya: std::marker::PhantomData,
            _dummyb: std::marker::PhantomData,
        }
    }
    fn new_single<F2: Fn(A) -> A>(f2: F2) -> Self {
        // This line is an error.
        Self::new(|x: A, y: B| (f2(x), y))
    }

    fn run(&self, a: A, b: B) -> (A, B) {
        (self.f)(a, b)
    }
}

pub fn main() {
    let works = Thing::new(|x: u32, y: u8| (x + 1, y + 1));
    let (_, _) = works.run(1u32, 2u8);

    // This line, the call site, is also an error.
    let broken = Thing::new_single(|x: u32| x + 1);
    let (_, _) = broken.run(0u32, 0u8);
}

The error around new_single is:

Self::new(|x: A, y: B| (f2(x), y))
   |         --------- ^^^^^^^^^^^^^^^^^^^^^^^ expected type parameter `F`, found closure

At the call site there's several pages of output, and it suggests:

 let broken: Thing<u32, u8, F> = Thing::new_single(|x: u32| x + 1);

And there's probably something to that. I can see how with the right amount of type annotations (not just F) this could compile. But the point of creating new_single() is to make the call site easier, not harder.

Pasting this question into ChatGPT it does fix the first problem, by changing new_single to:

    fn new_single<F2>(f2: F2) -> Thing<A, B, impl Fn(A, B) -> (A, B)> {
        Thing::new(move |x: A, y: B| (f2(x), y))
    }

But the call site still breaks with the same error, saying it needs type annotations.

You'd think I could fix the call site with this, but no that's not enough:

    let broken = Thing::<_,u8,_>::new_single(|x: u32| x + 1);

Also ineffective: specifying u8 in B's place in new_single:

    fn new_single<F2>(f2: F2) -> Thing<A, u8, impl Fn(A, u8) -> (A, u8)>
    where
        F2: Fn(A) -> A,
    {
        Thing::new(move |x: A, y: u8| (f2(x), y))
    }

Edit: An ugly workaround using specialization

Is the only downside to this thing I came up with that documentation will imply that it'll only work for u8, while actually because of the new_thing generic args that it'll work for anything?

use std::fmt::Debug;

struct Thing<A, B, F>
where
    A: Debug,
    B: Debug,
    F: Fn(A, B) -> (A, B),
{
    f: F,
    _dummya: std::marker::PhantomData<A>,
    _dummyb: std::marker::PhantomData<B>,
}

impl<A, B, F> Thing<A, B, F>
where
    A: Debug,
    B: Debug,
    F: Fn(A, B) -> (A, B),
{
    fn new(f: F) -> Self {
        Self {
            f,
            _dummya: std::marker::PhantomData,
            _dummyb: std::marker::PhantomData,
        }
    }
    fn run(&self, a: A, b: B) -> (A, B) {
        (self.f)(a, b)
    }
}

impl Thing<u8, u8, fn(u8, u8) -> (u8, u8)> {
    fn new_single<A, F2>(f2: F2) -> Thing<A, u8, impl Fn(A, u8) -> (A, u8)>
    where
        A: Debug,
        F2: Fn(A) -> A,
    {
        Thing::new(move |x: A, y: u8| (f2(x), y))
    }
}

pub fn main() {
    let works = Thing::new(|x: u32, y: u8| (x + 1, y + 1));
    let (_, _) = works.run(1u32, 2u8);

    let no_longer_broken = Thing::new_single(|x: u32| x + 1);
    let (_, _) = no_longer_broken.run(0u32, 0u8);
}

I can make the documentation a bit better by creating a fake type whose only purpose is to be ignored, but has a name like AnyType which would help readers of documentation understand that it's not just valid for that one type.


Solution

  • So the full error message is actually helpful here, and gives a clue as to what you're doing wrong, which I've bolded:

       |
    14 | impl Thing
       |            - expected this type parameter
    ...
    29 |         Self::new(|x: A, y: B| (f2(x), y))
       |         --------- ^^^^^^^^^^^^^^^^^^^^^^^ expected type parameter `F`, found closure
       |         |
       |         arguments to this function are incorrect
       |
       = note: expected type parameter `F`
                         found closure `{closure@src/main.rs:29:19: 29:31}`
       = help: every closure has a distinct type and so could not always match the caller-chosen type of parameter `F`
    

    The basic problem is right at the start of the impl block – it's written impl<A, B, F>. That means that everything inside the impl block is dependent on an A, B and F chosen by the caller, and has to use the caller-chosen values of A, B and F.

    Your original attempt at new_single is declared as fn new_single<F2: Fn(A) -> A>(f2: F2) -> Self, i.e. it returns a Self which is a Thing<A, B, F>. So, it's specifying that the function has to return a Thing whose closure is some specific user-provided closure. There's no way to do that without accepting the closure from the user.

    Instead, you need to specify the return type of the function correctly – it's returning a Thing whose F is chosen by the function, not by the user. That looks like this:

    fn new_single<A: Debug, B: Debug>(f2: impl Fn(A) -> A)
        -> Thing<A, B, impl Fn(A, B) -> (A, B)> {
        Thing::new(move |x: A, y: B| (f2(x), y))
    }
    

    I made three changes. One is to get the return type correct: the third type argument to Thing is impl Fn(A, B) -> (A, B), because it is a function from pairs to pairs and the specific function is chosen by new_single, not by the caller. The other change is that the closure has to own f2 rather than borrow it, specified by the move keyword – otherwise f2 gets deallocated when new_single returns and any attempt to use the resulting Thing is a use-after-free. (Apparently ChatGPT has seen this pattern somewhere before, because it made the same changes.) The third is to make the function generic on A and B, rather than borrowing the A and B generic parameters from the impl block, as it needs to be moved outside the impl block.

    The reason to move the function outside the impl block is that it won't work correctly if you put it in an impl<A, B, F> block – the problem is that Rust assumes from the bounds on the impl block that it needs to know an A, B and F in order to be able to call the function, and because F is no longer used or mentioned anywhere in the function or in its caller, Rust has no way to figure out what F to use. It's irrelevant, but by writing impl<…, F> you told Rust that it was relevant, and as such it will complain if it can't figure the value out. All your attempts to call the function were failing because Rust couldn't determine what value of F the caller was trying to use.

    I don't think it's possible to fix this if you keep new_single as an associated item on Thing; the problem is that the impl block declaration would have to be impl<A: Debug, B: Debug> Thing<A, B, impl Fn (A, B) -> (A, B)> but that isn't valid Rust (you can't put an impl in an implementation header). It will work fine, however, if you just make new_single a function at the top level rather than putting it inside an impl block. (impl blocks are basically for things that are callable using some concrete type, but with this function, you don't have a concrete Thing<A, B, F> to call it on because the caller doesn't know F, so as a consequence you can't put it inside an impl block.) Often people use impl blocks as, effectively, a form of namespacing, but that isn't actually how they're designed, and they won't work when the caller doesn't know all the details of the type they're calling the function/method on because then they wouldn't be able to select the exact correct namespace.

    So the solution is just to move the function outside the impl block; and then (with the impl Fn return value, move on the closure, and A/B/F2 as the only generic parameters) everything will work correctly.