Struct holding closure in `#![no_std]`

Hello. I have a problem with Rust closures, namely holding them inside a struct.

I am writing a library to draw on an analog oscilloscope screen using a Raspberry Pi Pico (thumbv6m-none-eabi). I have the following struct for drawing a path defined by a parametric equation:

pub struct ParametricPath<F>
where F: Fn(f32) -> (f32, f32) {
    t_0: f32,
    t_step: f32,
    t_1: f32,
    us: u32,
    function: F,
}

which while drawing calls the function to calculate consecutive points of the curve (performance sensitive).

This all works beautifully when i simply create a ParametricPath object:

let mut u = 0.;
loop {
    let lissajous = ParametricPath::new(0., 0.04, 2. * PI, 0, |t| {
        (3.* libm::sinf(t), 3. * libm::sinf(2. * t))
    });
    display.draw(&lissajous);
    u += 0.05;
}

produces a beautiful spinning Lissajous curve.

The problem arises when I want to create a method to create a circle (ParametricPath::circle), like this:

pub fn circle(o: (f32, f32), r: f32, t_step: f32, us: u32) -> Self {
    let f = |t| {
        let (y, x) = libm::sincosf(t);
        (r * (x - o.0), r * (y - o.1))
    };
    Self {
        t_0: 0.,
        t_step,
        t_1: 2. * PI,
        us,
        function: f,
    }
}

The compiler complains that he "expected type parameter F : found closure {closure@src/drawable/parametric_path.rs:33:17: 33:20}". I don't fully understand why passing in a closure while creating Lissajous figures worked, whereas here I receive an error.

Is there a way to overcome this error? Keep in mind this is a #![no_std] environment, I would really like to avoid Box<dyn Fn ...>, it requires me to bring in an allocator, and I don't like the performance overhead introduced by dynamic dispatch.

Thank you in advance for your help :slight_smile:

closures have compiler synthesized types which cannot be named by the programmer, which means a function cannot return concrete types containing closures.

in other words, your ParametricPath type can never be a function's return type. the only way to return something made up out of a closure is to return an opaque type using RPIT, but then you need to access your data through a trait.


EDIT:

you can use RPIT for the parameter type only, no need to return an entirely opaque type, see posts below by @quinedot

following is my original answer using a trick combinding the Deref trait and unsize coercion to return a opaque type.

END_OF_EDIT


in this particular case, I think a simple workaround like the following could work, thanks to unsize coercion:

// add `?Sized` bound to F
pub struct ParametricPath<F>
where F: Fn(f32) -> (f32, f32) + ?Sized {
    // ...
}
// the trick: coercion from generic type to a concrete type
// here the `Fn` trait object is perfect
// I belive the `'static` bound can be lifted but the struct definition must be changed
impl<F> Deref for ParametricPath<F> where F: Fn(f32) -> (f32, f32) + 'static {
    type Target = ParametricPath<dyn Fn(f32) -> (f32, f32)>;
    fn deref(&self) -> &ParametricPath<dyn Fn(f32) -> (f32, f32)> {
        self
    }
}
// now the `circle()` constructor can use RPIT
pub fn circle(
    o: (f32, f32),
    r: f32,
    t_step: f32,
    us: u32,
) -> impl Deref<Target = ParametricPath<dyn Fn(f32) -> (f32, f32)>> {
    // added `move` keyword to capture by value
    // although the captured variables are all `Copy`, the default rule is
    // to capture by reference
    let f = move |t| {
        let (y, x) = libm::sincosf(t);
        (r * (x - o.0), r * (y - o.1))
    };
    ParametricPath {
        t_0: 0.,
        t_step,
        t_1: 2. * PI,
        us,
        function: f,
    }
}

Hi, thank you for the fast reply!

Your solution compiles, but I think it just moves the compile-time problem to constructing the object: when i try

let circle = ParametricPath::circle((0., 0.), 2., 0.1, 1);

the compiler demands that I specify the generic argument:

error[E0284]: type annotations needed
  --> src/main.rs:89:22
   |
89 |         let circle = ParametricPath::circle((0., 0.), 2., 0.1, 1);
   |                      ^^^^^^^^^^^^^^^^^^^^^^ cannot infer type of the type parameter `F` declared on the struct `ParametricPath`
   |
   = note: cannot satisfy `<_ as FnOnce<(f32,)>>::Output == (f32, f32)`
note: required by a bound in `ParametricPath::<F>::circle`
  --> src/drawable/parametric_path.rs:28:19
   |
28 |     F: Fn(f32) -> (f32, f32),
   |                   ^^^^^^^^^^ required by this bound in `ParametricPath::<F>::circle`
...
40 |     pub fn circle(o: (f32, f32), r: f32, t_step: f32, us: u32) -> impl Deref<Target = ParametricPath<dyn Fn(f32) -> (f32, f32)>> {
   |            ------ required by a bound in this associated function
help: consider specifying the generic argument
   |
89 |         let circle = ParametricPath::<F>::circle((0., 0.), 2., 0.1, 1);
   |        

As far as I understand, this would again require me to know the type of the closure - which I cannot do.

There is also one more thing I don't understand: since you said that ParametricPath cannot be a function's return type, why does my function

pub fn new(t_0: f32, t_step: f32, t_1: f32, us: u32, function: F) -> Self {
    Self {
        t_0,
        t_step,
        t_1,
        us,
        function,
    }
}

work as expected?

that's a different error message, because you are defining the constructor function as an associated function of an generic type ParametricPath<F>, but the generic argument cannot be inferred in anyway.

you can simple move the function out of the impl block, to module scope, or if you prefer the ParametricPath::circle() syntax, you can define it for a concrete type, for example:

// this example choose the function pointer type as the concrete implementation, you can choose other type, for example, the `Fn` trait object.
impl ParametricPath<fn(f32) ->(f32, f32)> {
    pub fn circle(...) -> impl Deref<Target = ...> {
        //...
    }
}

You can return a ParametricPath<_>, you just can't name the parameter F. You can use return-position impl Trait (RPIT) to return an opaque parameter, instead:

impl<F> ParametricPath<F> {
    pub fn circle(o: (f32, f32), r: f32, t_step: f32, us: u32) 
        -> ParametricPath<impl Fn(f32) -> (f32, f32)>
        //                ^^^^^^^^^^^^^^^^^^^^^^^^^^
    {
        //      vvvv Added for unrelated reasons
        let f = move |t| {
            let (y, x) = libm::sincosf(t);
            (r * (x - o.0), r * (y - o.1))
        };
        ParametricPath {
            t_0: 0.,
            t_step,
            t_1: 2. * PI,
            us,
            function: f,
        }
    }
}

This is similar to @nerditation's approach, but the impl Trait need not be the outermost type, and thus you don't need the impl Deref trick.

(I also removed the bounds on F from the struct declaration so that they don't have to be repeated everywhere.)

1 Like

Hi, thank you all to the replies! Thanks to your help I managed to solve my problem, my solution looks like this (for people who come across this thread in the future):

use core::f32::consts::PI;

pub struct ParametricPath<F> {
    t_0: f32,
    t_step: f32,
    t_1: f32,
    us: u32,
    function: F,
}

impl<F> ParametricPath<F>
where F : Fn(f32) -> (f32, f32) {
    fn new(t_0: f32, t_step: f32, t_1: f32, us: u32, function: F) -> Self {
        Self {
            t_0,
            t_step,
            t_1,
            us,
            function,
        }
    }
}

impl ParametricPath<fn(f32) -> (f32, f32)> {
    pub fn circle(o: (f32, f32), r: f32, t_step: f32, us: u32) -> ParametricPath<impl Fn(f32) -> (f32, f32)> {
        let f = move |t| {
            let (y, x) = libm::sincosf(t);
            (r * (x - o.0), r * (y - o.1))
        };
        
        ParametricPath {
            t_0: 0.,
            t_step,
            t_1: 2. * PI,
            us,
            function: f,
        }
    }
}

fn main() {
    let mut u = 0.;
    loop {
        let lissajous = ParametricPath::new(0., 0.04, 2. * PI, 0, |t| {
            (3. * libm::sinf(t), 3. * libm::sinf(2. * t))
        });
        
        let circle = ParametricPath::circle((0., 0.), 0., 0.1, 1);
        // display.draw(&lissajous);
        // display.draw(&circle);
        u += 0.05;
    }
}

From what I understood about the nature of the problem (correct me if I'm wrong):

  • each closure has a distinct type which cannot be named
  • therefore, when returning a closure (or something containing a closure), we cannot specify the concrete return type - that's why we use impl Fn(...) -> ..., to signify we are returning something implementing the Fn(...) -> ... trait
  • the most confusing part for me - the type annotations needed error I encountered when attempting to run ParametricPath::circle(...) with the function implemented as:
impl<F> ParametricPath<F>
where F : Fn(f32) -> (f32, f32) {
    fn circle(...) -> ParametricPath<impl Fn(f32) -> (f32, f32)> {
       // ...
    }
}

is due to the compiling not able to infer the type F. What's odd to me, is that we don't actually use the value of F in the function, as we don't return Self but ParametricPath<impl Fn(f32) -> (f32, f32)>. I fixed this problem by changing the circle metod's impl block signature to impl ParametricPath<fn(f32) -> (f32, f32)>, to provide a concrete value for the type, just so that the compiler doesn't complain. But, since we don't actually use this type, we could potentially put any type name here (I tried it with impl ParametricPath<i32>) and it worked just fine. Seems odd that Rust doesn't include syntax to deal with such situations.

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.