Novice Question: Dynamic Dispatch with Borrowing (as might occur in a loop)

trait Eqns {
    fn d(&self, y: & f64)->f64;
}

struct Model {}

impl Eqns for Model {
    fn d(&self, _y: & f64)->f64 { 
        200.0
    }
}

fn euler(m : Box<dyn Eqns>, y : &mut f64) {
   *y = *y + m.d(y);
}

// Goal is to uncomment line with 0382 error.
// I know how to get this to work using static dispatch,
// but I want to use dynamic dispatch and trait objects.
// How can I get definition of fn euler and call to fn euler to work 
// by borrowing mm. 
// I don't want to clone mm and don't need to modify it.
// I've tried
//     fn euler (m : & Box...
//     ...
//.    euler(&mm, &mut yy)
fn main () {
    let mut yy:f64 = 0.0;
    let mm = Box::new(Model{});
    euler(mm, &mut yy);
    // I would actually call euler(mm) many times in a loop; but the next call shows borrow issue
    // euler(mm, &mut yy);  //0382 error
    println!("yy is: {:#?}", yy);
}



(Playground)

Output:

yy is: 200.0

Errors:

   Compiling playground v0.0.1 (/playground)
    Finished dev [unoptimized + debuginfo] target(s) in 1.35s
     Running `target/debug/playground`

Step 1

The way you pass a trait object without transferring ownership by reference:

fn euler(m: &dyn Eqns, y: &mut f64) {

For example. I've gotten rid of the box temporarily; I could have kept it but it's less ergonomic and isn't actually needed in this bit of code.

Step 2

You can make this a method of the Eqns trait.

trait Eqns {
    fn d(&self, y: &f64) -> f64;
    fn euler(&self, y: &mut f64) {
        *y += self.d(y);
    }
}

For example.

Step 3

dyn Eqns implements Eqns, and in that case, the &self of Eqns::euler is a &dyn Eqns, like in step 1.

Method call resolution on a Box<dyn Eqns> will do auto-deref and properly pass a &dyn Eqns.

So you can ergonomically go back to Box<dyn Eqns> if you want to.

2 Likes

Thanks very much for your answer.

My question, as you probably guessed, is extracted from a larger program for simulating 10-20 coupled ordinary differential equations using a much more complicated method than euler. Since I want to use euler (and its more complicated sibling) in other projects, I don't want euler to be part of the Eqns trait unless that is absolutely required.

Regarding your Step 1, I tried leaving the Box in, adding the &, and defining euler as follows

fn euler(m: &Box<dyn Eqns>, y: &mut f64) 

But then, how to call it? Trying

euler(&mm, &mut yy);

resulted in an error. See playground link

  Compiling playground v0.0.1 (/playground)
error[E0308]: mismatched types
  --> src/main.rs:20:11
   |
20 |     euler(&mm, &mut yy);
   |     ----- ^^^ expected `&Box<dyn Eqns>`, found `&Box<Model>`
   |     |
   |     arguments to this function are incorrect
   |
   = note: expected reference `&Box<(dyn Eqns + 'static)>`
              found reference `&Box<Model>`
note: function defined here
  --> src/main.rs:13:4
   |
13 | fn euler(m : &Box<dyn Eqns>, y : &mut f64) {
   |    ^^^^^ ------------------

I will experiment with adding euler to the trait if I have to, but I would prefer keeping eqns separate if possible. Any idea where my error is,? Or is my approach not workable? Again thank you for your interest and comments.

The function signature still says &Box<_>. If the function signature expects a Box, but you don't pass one, then of course you'll get a type error.

You don't need that. Drop the box, &dyn Trait is sufficient for temporary access to a trait object. This compiles.

1 Like

Looks like you made two changes, getting rid of Box in two different places. The playground link you provide does compile for me, so thank you. I will proceed with my refactoring of my larger code along these lines, seeing if the need for Box arises.

It's not required, but I don't understand this logic at all. If you have a method that takes a &dyn Eqns or Box<dyn Eqns>, it clearly can't work unless trait Eqns is included in the project.

Anyway, here's my last playground modified to extract the method to be a standalone function. The loss of method call syntax results in a loss of some ergonomics.

    euler(&*mm, &mut yy);
    euler(&*mm, &mut yy);

Or maybe you're looking for the extension trait pattern so that euler can live in a different crate.

I hear what you are saying about Eqns and Euler being co-dependent, and after more reflection, I am considering that approach. I was trying to place the trait definition in a separate file and all my ODE solvers (and other numerical methods) in another. But as you point out, they are connected.

Your new playground was particularly educational because you kept Box in the calls of euler, but didn't keep it in the euler signature. Clearly, you were just showing how it Box could be handled if I needed to keep for other reasons not apparent in my simplified example.

I was just reading about the extension trait pattern and trying to wrap my head around it and its possible utility in my current development project.

Both answers to my question constitute solutions, but I could only mark one.

Well, I was assuming you wanted to get rid of the Box, which seemed like the right solution, as it is not needed at all based on the code you posted.

Of course, can keep the wrapping Box and make the function accept a regular reference only, then obtain a normal reference from the box by dereferencing it (and re-referencing the inner value). But you obviously can't do the opposite: you can't keep the Box in the signature and then supply a plain reference.

I learned from your reply that Box was not necessary in this situation and in fact have removed it from my larger program. Was trying to understand (and extend to my own problem space), the examples in Chapter 17 of The Book and the tradeoffs between static dispatch (with generic functions) and dynamic dispatch (with trait bounds). For some reason, most every example of dynamic dispatch I found was for a collection of items. Then I think Box is useful allowing the collection to hold disparate types (related via a trait). However, my thought was if that appraoch works for disparate types, it should work for a single instance of a single type. And I was trying to work through the syntax for that. Both replies clarified and then increased my understanding.

The reason is probably that generics are the default tool when you're only dealing with a single value like an argument,[1] but collections are a common case where you must support disparate types via type erasure.

That type erasure is dyn Trait. You can put that in a Box, but also in an Arc, behind a &, etc.


  1. Why (probably) allocate, incur dispatch indirection, and inhibit type-specific optimization if you don't need to? (Possible retort: code size and/or compile time matter a lot for my use case, so I want to reduce monomorphization.) ↩ī¸Ž

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.