New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Lazy Objects and Lookups #160
Comments
I'm starting to favor the utilities in use minijinja::value::{StructObject, Value};
use minijinja::{context, Environment};
use minijinja_stack_ref::scope;
struct Config {
version: &'static str,
}
struct State {
config: Config,
}
impl StructObject for Config {
fn get_field(&self, field: &str) -> Option<Value> {
match field {
"version" => Some(Value::from(self.version)),
_ => None,
}
}
}
impl StructObject for State {
fn get_field(&self, field: &str) -> Option<Value> {
match field {
// return a reference to the inner config through reborrowing
"config" => Some(reborrow(self, |slf, scope| {
scope.struct_object_ref(&slf.config)
})),
_ => None,
}
}
}
let mut env = Environment::new();
env.add_template(
"info",
"app version: {{ state.config.version }}"
)
.unwrap();
let state = State {
config: Config {
version: env!("CARGO_PKG_VERSION"),
}
};
let rv = scope(|scope| {
let tmpl = env.get_template("info").unwrap();
tmpl.render(context! {
state => scope.struct_object_ref(&state),
}).unwrap()
});
println!("{}", rv); |
I'll give |
Finally had a chance to try it out. Unfortunately it doesn't quite suffice. Here's something analogous to what I'd like to make work: struct CompoundValue {
seq: Vec<CompoundValue>,
map: std::collections::HashMap<String, CompoundValue>,
}
impl StructObject for CompoundValue {
fn get_field(&self, name: &str) -> Option<Value> {
match name {
"seq" => Some(reborrow(self, |this, scope| {
scope.seq_object_ref(CompoundValueSeq::ref_cast(&this.seq))
})),
"map" => Some(reborrow(self, |this, scope| {
scope.struct_object_ref(CompoundValueMap::ref_cast(&this.map))
})),
_ => None
}
}
}
#[derive(RefCast)]
#[repr(transparent)]
struct CompoundValueSeq(Vec<CompoundValue>);
impl SeqObject for CompoundValueSeq {
fn get_item(&self, idx: usize) -> Option<Value> {
// Since the second argument must be `fn`, this fails.
reborrow(self, move |this, scope| {
this.0.get(idx).map(|v| scope.struct_object_ref(v))
})
// Maybe an API by which we can pass some ctxt?
// reborrow(self, idx, |this, scope, idx| {
// this.0.get(idx).map(|v| scope.struct_object_ref(v))
// })
}
fn item_count(&self) -> usize {
self.0.len()
}
}
#[derive(RefCast)]
#[repr(transparent)]
struct CompoundValueMap(std::collections::HashMap<String, CompoundValue>);
impl StructObject for CompoundValueMap {
fn get_field(&self, name: &str) -> Option<Value> {
// The same thing here.
reborrow(self, |this, scope| {
this.0.get(name).map(|v| scope.struct_object_ref(v))
})
// Same thing with the context, but note that Ctxt: Copy would not work.
// reborrow(self, name, |this, scope, name| {
// this.0.get(name).map(|v| scope.struct_object_ref(v))
// })
}
} In short, with One potential fix is to allow a context to be passed in along with the In all, I'm finding this API to be somewhat laborious to use and at the same time wondering if different design decisions for the internals of |
Unfortunately I'm not sure if there is a way to make this work with
I'm not sure. I agree that the API is pretty stupid and also the entire reborrowing system only works if the value was placed in the context in the first place via the scope guard. About different APIs I'm also entirely unsure how this could be built. I don't actually think that the MiniJinja internals are the limitation here. Even if hypothetically Value were to have a lifetime, I can't imagine how that would be in any way helpful. I think it needs some form of runtime checks for borrows. I will try to brainstorm on what can be done. |
One obvious thing here would be to store struct CompoundValue {
seq: Vec<CompoundValue>,
map: HashMap<String, CompoundValue>,
}
impl StructObject for CompoundValue {
fn get_field<'a>(&'a self, name: &str) -> Option<Value<'a>> {
match name {
"seq" => Value::from_ref(&this.seq),
"map" => Value::from_ref(&this.map),
_ => None
}
}
} |
The issue is the |
I'm not sure I follow. I'm suggesting that there doesn't need to be any unsafe here. As far as I can tell, For the latter case, logically speaking, as long as the value is valid immediately before and during template rendering, everything is okay. Whether the value becomes invalid after shouldn't matter as rendering has completed. This is exactly what borrows in Rust capture, and the straightforward use of references in I suggest that there's an issue with the internal design because conflating the two uses of |
Other than If you have an API such as |
There is a hypothetically different world where each object leaks out to the end of the execution out of principle, and then borrows would be possible without thinking about lifetimes, but that would most likely be an entirely different experience and I‘m not sure what the consequences of that are (other than potentially very high memory usage). |
Then it sounds like the |
I'm no longer planning on resolving this. The change is too invasive for the engine. While most of my lifetime concerns are resolved now, the |
Would you accept a PR that implements the needed changes, @mitsuhiko? |
@SergioBenitez I would not object if you find a way! |
I imagine such a way exists! Though I do expect it to be rather invasive. Before I embark, I want to make sure that you're not opposed to the change, or the potential invasiveness of such a change, ideologically. Do you think that's the case? |
@SergioBenitez I'm not opposed to potentially invasive changes for as long as it does not fundamentally restrict the language or user experience. |
Awesome! I'll have something for you to look at soon-ish. |
Isolated from #157:
In the latter case, I have imagined an API by which the engine tracks object path accesses at compile-time and then asks once (say via an Object trait) for a terminal value that isn't compound, so anything but a map or sequence. For example, for
foo.bar[0].baz
, the engine would ask thefoo
object (assuming it is one) forbar[0].baz
. The object can then perform an efficient lookup without any clones and return the terminal baz. Similarly, if we have:The engine would again ask foo for
bar[0].baz
. This of course requires some thought and implementation grease, but, aside from allowing full borrows of anything, seems like the most sensible approach.The text was updated successfully, but these errors were encountered: