askama icon indicating copy to clipboard operation
askama copied to clipboard

Cannot move out of dereference when mixing loops with locks or similar

Open mensi opened this issue 4 years ago • 4 comments

I have some shared state where I'd like to take the lock, render the template and release the lock all ideally without too much stuff being copied around. For example:

use std::collections::HashMap;
use std::sync::{RwLock, RwLockReadGuard};
use askama::Template;

#[derive(Template)]
#[template(source = "{% for v in foo.values() %}{{ v }}{% endfor %}", ext = "txt", print = "code")]
struct Example<'a> {
    foo: RwLockReadGuard<'a, HashMap<u32, String>>
}

fn main() {
    let lock = RwLock::new(HashMap::new());
    let example = Example {
        foo: lock.read().expect("Lock poisoned")
    };
    println!("{}", example.render().expect("Rendering failed"));
}

This does not work:

error[E0507]: cannot move out of a shared reference
 --> src\main.rs:5:10
  |
5 | #[derive(Template)]
  |          ^^^^^^^^ move occurs because value has type `std::collections::hash_map::Values<'_, u32, std::string::String>`, which does not implement the `Copy` trait

The generated code looks like this:

use std::collections::HashMap;
use std::sync::{RwLock, RwLockReadGuard};
use askama::Template;

//#[derive(Template)]
//#[template(source = "{% for v in foo.values() %}{{ v }}{% endfor %}", ext = "txt", print = "code")]
struct Example<'a> {
    foo: RwLockReadGuard<'a, HashMap<u32, String>>
}

fn main() {
    let lock = RwLock::new(HashMap::new());
    let example = Example {
        foo: lock.read().expect("Lock poisoned")
    };
    println!("{}", example.render().expect("Rendering failed"));
}

impl < 'a > ::askama::Template for Example< 'a > {
    fn render_into(&self, writer: &mut ::std::fmt::Write) -> ::askama::Result<()> {
        for (v, _loop_item) in ::askama::helpers::TemplateLoop::new((&self.foo.values()).into_iter()) {
            write!(
                writer,
                "{expr0}",
                expr0 = &::askama::MarkupDisplay::new_unsafe(&v, ::askama::Text),
            )?;
        }
        Ok(())
    }
    fn extension(&self) -> Option<&'static str> {
        Some("txt")
    }
    fn size_hint(&self) -> usize {
        0
    }
}
impl < 'a > ::askama::SizedTemplate for Example< 'a > {
    fn size_hint() -> usize {
        0
    }
    fn extension() -> Option<&'static str> {
        Some("txt")
    }
}
impl < 'a > ::std::fmt::Display for Example< 'a > {
    fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
        ::askama::Template::render_into(self, f).map_err(|_| ::std::fmt::Error {})
    }
}

Now I'm quite new to rust, so I don't quite understand what the reason is behind taking a reference in the loop expression. What I assume the intention is, is that (&{}).into_iter() is there to make sure whatever is passed in is either an Iterator or becomes one. However, reading up on IntoIterator, I'd expect something more like IntoIterator::into_iter({}). Swapping it in the generated code seems to work:

use std::collections::HashMap;
use std::sync::{RwLock, RwLockReadGuard};
use askama::Template;

//#[derive(Template)]
//#[template(source = "{% for v in foo.values() %}{{ v }}{% endfor %}", ext = "txt", print = "code")]
struct Example<'a> {
    foo: RwLockReadGuard<'a, HashMap<u32, String>>
}

fn main() {
    let lock = RwLock::new(HashMap::new());
    let example = Example {
        foo: lock.read().expect("Lock poisoned")
    };
    println!("{}", example.render().expect("Rendering failed"));
}

impl < 'a > ::askama::Template for Example< 'a > {
    fn render_into(&self, writer: &mut ::std::fmt::Write) -> ::askama::Result<()> {
        for (v, _loop_item) in ::askama::helpers::TemplateLoop::new(IntoIterator::into_iter(self.foo.values())) {
            write!(
                writer,
                "{expr0}",
                expr0 = &::askama::MarkupDisplay::new_unsafe(&v, ::askama::Text),
            )?;
        }
        Ok(())
    }
    fn extension(&self) -> Option<&'static str> {
        Some("txt")
    }
    fn size_hint(&self) -> usize {
        0
    }
}
impl < 'a > ::askama::SizedTemplate for Example< 'a > {
    fn size_hint() -> usize {
        0
    }
    fn extension() -> Option<&'static str> {
        Some("txt")
    }
}
impl < 'a > ::std::fmt::Display for Example< 'a > {
    fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
        ::askama::Template::render_into(self, f).map_err(|_| ::std::fmt::Error {})
    }
}

So in the code generation, instead of:

            _ => buf.writeln(&format!(
                ", _loop_item) in ::askama::helpers::TemplateLoop::new((&{}).into_iter()) {{",
                expr_code
            )),

wouldn't:

            _ => buf.writeln(&format!(
                ", _loop_item) in ::askama::helpers::TemplateLoop::new(IntoIterator::into_iter({})) {{",
                expr_code
            )),

be better? Or am I missing something?

mensi avatar May 22 '20 20:05 mensi

Only using a shared reference to the template context type during template rendering is an invariant that I've kept in mind from the beginning, in the hope that this would at some point enable stuff like functional recursive template rendering. So this is very much by design.

djc avatar May 22 '20 21:05 djc

A possible workaround here would be to create a view type that holds references into the original type and derive the Template on that. Hope that helps.

djc avatar May 23 '20 19:05 djc

In terms of workarounds I actually went for use std::ops::Deref; and then using .deref() in the template. But to me the question remains: self is already taken by reference in render_into, why take another reference and call into_iter() instead of what supposedly rust does for for loops with calling the IntoIterator trait explicitly? This would also avoid calling into_iter that are not IntoIterator trait implementations.

mensi avatar May 23 '20 19:05 mensi

I think the canonical issue for this is #107. It's kind of complex to correctly model the interactions with IntoIterator and Iterator itself, and without specialization I don't think there's much that can be improved. If you come up with a concrete proposal I'd be very interested though!

djc avatar May 23 '20 20:05 djc