0

Using rustc 1.10.0, I'm trying to write some code which passes around boxed closures--the eventual goal is to procedurally generate an animation of fractals. Right now I have some function signatures like this:

pub fn interpolate_rectilinear(width: u32, height: u32, mut min_x: f64, mut max_x: f64, mut min_y: f64, mut max_y: f64)
    -> Box<Fn(u32, u32) -> Complex64 + Send + Sync + 'static> { ... }

pub fn interpolate_stretch(width: u32, height: u32, mut min_x: f64, mut max_x: f64, mut min_y: f64, mut max_y: f64)
    -> Box<Fn(u32, u32) -> Complex64 + Send + Sync + 'static> { ... }

pub fn parallel_image<F>(width: u32, height: u32, function: &F, interpolate: &Box<Fn(u32, u32) -> Complex64 + Send + Sync>, threshold: f64)
    -> ImageBuffer<image::Luma<u8>, Vec<u8>>
        where F: Sync + Fn(Complex64) -> Complex64
{ ... }

pub fn sequential_image<F>(width: u32, height: u32, function: &F, interpolate: &Box<Fn(u32, u32) -> Complex64>, threshold: f64)
    -> ImageBuffer<image::Luma<u8>, Vec<u8>>
        where F: Fn(Complex64) -> Complex64
{ ... }

Running this code for one image at a time in a binary works without problems:

let interpolate = interpolate_rectilinear(width, height, -1.0, 1.0, -1.0, 1.0);
let image = parallel_image(width * 2, height * 2, &default_julia, &interpolate, 2.0);

However, I wanted to ensure my serial and parallel image-production were both producing the same results, so I wrote the following test function:

#[test]
fn test_serial_parallel_agree() {
    let (width, height) = (200, 200);
    let threshold = 2.0;
    let interpolate = interpolate_stretch(width, height, -1.0, 1.0, -1.0, 1.0);

    assert!(parallel_image(width, height, &default_julia, &interpolate, threshold)
        .pixels()
        .zip(sequential_image(width, height, &default_julia, &interpolate, threshold)
            .pixels())
        .all(|(p, s)| p == s));
}

This refuses to compile, and I just can't figure it out. The error it gives is as follows:

> cargo test
Compiling julia-set v0.3.0 
src/lib.rs:231:66: 231:78 error: mismatched types [E0308]
src/lib.rs:231             .zip(sequential_image(width, height, &default_julia, &interpolate, threshold)
                                                                                ^~~~~~~~~~~~
src/lib.rs:229:9: 233:36 note: in this expansion of assert! (defined in <std macros>)
src/lib.rs:231:66: 231:78 help: run `rustc --explain E0308` to see a detailed explanation
src/lib.rs:231:66: 231:78 note: expected type `&Box<std::ops::Fn(u32, u32) -> num::Complex<f64> + 'static>`
src/lib.rs:231:66: 231:78 note:    found type `&Box<std::ops::Fn(u32, u32) -> num::Complex<f64> + Send + Sync>`
error: aborting due to previous error
Build failed, waiting for other jobs to finish...
error: Could not compile `julia-set`.

I really don't know what's going on there. I don't know why I'm required to manually mark Send and Sync in the boxed return types of the interpolation functions, when the compiler typically derives those traits automatically. Still, I just kept adding in markers that the compiler suggested until things worked.

The real problem is that, while I think I have a pretty good guess why you can't just mark a boxed closure 'static, I don't know what's requiring that lifetime in this case or how to fix it.

I did guess that possibly the issue was that I was trying to reference the closure from two read-borrows at once, (which should be ok, but I was desperate); at any rate, wrapping interpolate in an Rc gives the exact same error, so that wasn't the problem.

coriolinus
  • 879
  • 2
  • 8
  • 18
  • Do you have a git repo or somesuch? I'd like to test this. – Veedrac Aug 01 '16 at 19:04
  • In addition to Veedrac's comment, you should provide a [MCVE] when asking a question here on Stack Overflow (really anywhere, but especially here). If we can't reproduce your problem purely from what is included **here**, the question will end up closed. – Shepmaster Aug 01 '16 at 21:11
  • @Veedrac Yes, the repository is [here](https://github.com/coriolinus/julia-set/tree/c3b1d31ec9bc981f1a0575c7e3fcc2d377de30db). The location where the call works is [here](https://github.com/coriolinus/julia-set/blob/master/src/bin.rs#L79-L82), and the test where it doesn't work is [here](https://github.com/coriolinus/julia-set/blob/master/src/lib.rs#L223-L234). I'd pasted as little as I did here because it seemed the question was already fairly long and detailed, and pasting several hundred lines of extraneous source wouldn't have been minimal. – coriolinus Aug 01 '16 at 22:42
  • 1
    @coriolinus: I think you misunderstood. The intention was that you reduce your problem until it is just a few lines long and doesn't require any crates. Most likely you end up answering your own question once you manage to reduce the issue. – oli_obk Aug 02 '16 at 08:52
  • @ker That's why the code I posted, above, is down to four function signatures, a two line invocation that works, and a 10 line invocation that doesn't, and reasoning about why that's confusing. It's as minimal as I could make it. You should be able to reproduce by copy-pasting the code shown and just leaving the function bodies `unimplemented!()`. The question is about the lifetimes, analysis of which is compile-time behavior. – coriolinus Aug 02 '16 at 10:25
  • 1
    Sadly this is not possible: https://play.rust-lang.org/?gist=5791a0b6245348b85d61fe6733ec38f6&version=stable&backtrace=0 there are still dependencies on other crates and on types not supplied here. We can obviously do this work ourselves, but you motivate help by providing such a "compileable" example. Obviously it won't compile, but it will produce the exact error you showed. – oli_obk Aug 02 '16 at 10:48
  • *You should be able to reproduce by copy-pasting the code shown and just leaving the function bodies unimplemented!()* — then put **that version of the code** into your question. However, if we still need other crates, then remove / replace them. As mentioned, we should be able to copy-paste what is in the question and produce the same error. – Shepmaster Aug 02 '16 at 12:00

1 Answers1

1

The problem is actually here:

pub fn sequential_image<F>(
    ...,
    interpolate: &Box<Fn(u32, u32) -> Complex64>,
    ...) -> ...

The interpolate doesn't expect a &Box<Fn(u32, u32) -> Complex64 + Send + Sync>, and Rust is pretty bad at handling variance through all of this complexity.

One solution is to do the cast where it's called:

sequential_image(width, height, &default_julia,
    &(interpolate as Box<Fn(u32, u32) -> Complex64>),
threshold)

but this requires a value case of sequential_image and is pretty damn ugly.

A nicer way is to just fix the parameter of sequential_image to something both more general and something easier for the compiler to reason about: basic pointers.

pub fn sequential_image<F>(
    ...,
    interpolate: &Fn(u32, u32) -> Complex64,
    ...) -> ...

Now you can call it with just

sequential_image(width, height, &default_julia,
    &*interpolate,
threshold)

and the compiler can do all of the variance magic itself.

Veedrac
  • 58,273
  • 15
  • 112
  • 169
  • The reason I started down the `Box` rabbithole in the first place. If I change the signature of `interpolate` as you suggest, `parallel_image` complains that `Fn(u32, u32) -> Complex64 cannot be shared between threads safely`. I try to resolve that by wrapping the function in an Arc: `let interpolate = Arc::new(interpolate);`, but that doesn't change the error. If instead I wrap it as `let interpolate = Arc::new(*interpolate);`, the error becomes `the trait bound std::ops::Fn(u32, u32) -> num::Complex: std::marker::Sized is not satisfied`. How else might I approach this? – coriolinus Aug 02 '16 at 12:47
  • I'd like `sequential_image` and `parallel_image` to share the same signature, because it shouldn't matter to the caller what happens under the hood. – coriolinus Aug 02 '16 at 12:48
  • @coriolinus `parallel_image` just needs to take `interpolate: &(Fn(u32, u32) -> Complex64 + Send + Sync)`. – Veedrac Aug 02 '16 at 12:56
  • Your first solution works; incidentally. thanks for showing me that! I hadn't realized adding `Send + Sync` would prevent the Box from working. I guess I'm looking for advice how to make both `sequential_image` and `parallel_image` work if their signature for `interpolate` is just the function pointer. – coriolinus Aug 02 '16 at 12:57
  • @coriolinus Actually `parallel_image` just needs `interpolate: &(Fn(u32, u32) -> Complex64 + Sync)`, not even `Send`. – Veedrac Aug 02 '16 at 13:03
  • oh, I see! And now that the signature doesn't expect a boxed function, having the `interpolate` implement `Send + Sync` doesn't require an explicit cast anymore. Thank you, this is exactly what I needed! – coriolinus Aug 02 '16 at 13:04
  • Is it possible to add trait bounds to a function argument like that in general, or is that a special case for functions? Until now I'd only known of the `function(t: T) {}` and `function(t: T) where T: Bound {}` syntaxes. – coriolinus Aug 02 '16 at 13:07
  • @coriolinus No, it's only special ones. I'm not sure of the exact rules, but I think it's only these: https://doc.rust-lang.org/1.5.0/std/marker/. – Veedrac Aug 02 '16 at 13:19