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
Object
but for sequences
#147
Comments
My preference would be for pub trait Object: fmt::Display + fmt::Debug + Any + Sync + Send {
// looks up an attribute by name
fn get_attr(&self, name: &str) -> Option<Value>;
// looks up an item by index
fn get_by_index(&self, idx: usize) -> Option<Value>;
// iterates over the attributes of the object
fn attributes(&self) -> Box<dyn Iterator<Item = &str> + '_>;
// returns the number of items in the object. Must be implemented
// for sequences, is entirely optional for mappings
fn len(&self) -> Option<usize>;
// what type of object is this? Valid return values are `Seq` and `Map`
fn kind(&self) -> ValueKind;
fn call_method(&self, state: &State, name: &str, args: &[Value]) -> Result<Value, Error>;
fn call(&self, state: &State, args: &[Value]) -> Result<Value, Error>;
} I'm not sure if |
I think this is feasible, but I would advocate against an API that dynamically asserts that an enum Key<'a> {
Attr(&'a str),
Index(usize),
}
pub trait Object: fmt::Display + fmt::Debug + Any + Sync + Send {
// looks up an attribute by name or index
fn get(&self, key: Key<'_>) -> Option<Value>;
// iterates over the attributes of the object
fn keys(&self) -> Box<dyn Iterator<Item = Key<'_>> + '_>;
fn call_method(&self, state: &State, name: &str, args: &[Value]) -> Result<Value, Error>;
fn call(&self, state: &State, args: &[Value]) -> Result<Value, Error>;
} If a value should be a |
So this is an interesting question. In MiniJinja they are equivalent today because we so far did not have objects that want to provide both. However in Jinja2 that was not possible because Python objects have attributes and keys. As such in Jinja2 Another difference in MiniJinja to Jinja2 is that I decided that I believe i like the MiniJinja semantics more and there is not much reason to emulate Jinja2 here. On the topic of keys: In the value type Generally speaking I think I prefer an object to be one trait that represents the entire object model similar to how Python objects work. Mostly because it's (as ugly as this can get) relatively easy to reason about. So what we know today is that both sequences and maps are things worth representing. We probably don't want to permit an object to impersonate a string and integer. In fact I don't see much value at the moment for an Object to represent anything but this. For the unlikely future case that something wants to proxy an entire value it's probably better to special case this and to let it return another value that it "becomes". However there is already a hole in the value type today where we just lie. The engine needs a Another benefit of having just one trait is that it lets an object to fully proxy something else at runtime too. This would be quite beneficial for some lazy loading scenarios. |
I'm starting to wonder if it wouldn't be saner to really expose the pub trait Object: fmt::Display + fmt::Debug + Any + Sync + Send {
fn kind(&self) -> ValueKind {
ValueKind::Map
}
fn get(&self, key: &Key) -> Option<Value> {
None
}
fn call_method(&self, state: &State, name: &str, args: &[Value]) -> Result<Value, Error> {
let _state = state;
let _args = args;
Err(Error::new(
ErrorKind::InvalidOperation,
format!("object has no method named {}", name),
))
}
fn call(&self, state: &State, args: &[Value]) -> Result<Value, Error> {
let _state = state;
let _args = args;
Err(Error::new(
ErrorKind::InvalidOperation,
"tried to call non callable object",
))
}
fn keys(&self) -> Box<dyn Iterator<Item = Key> + '_> {
Box::new(None.into_iter())
}
fn len(&self) -> usize {
self.keys().count()
}
fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl Object for MySeq {
fn kind(&self) -> ValueKind {
ValueKind::Seq
}
fn get(&self, key: &Key) -> Option<Value> {
if let Key::U64(idx) = key {
self.0.get(*idx as usize)
} else {
None
}
}
fn len(&self) -> usize {
self.0.len()
}
} The iteration behavior in the engine would be to iterate from Loop controller for instance then looks like this: impl Object for Loop {
fn keys(&self) -> Box<dyn Iterator<Item = Key> + '_> {
Box::new(
[
Key::Str("index0"),
Key::Str("index"),
Key::Str("length"),
Key::Str("revindex"),
Key::Str("revindex0"),
Key::Str("first"),
Key::Str("last"),
Key::Str("depth"),
Key::Str("depth0"),
]
.into_iter(),
)
}
fn get(&self, key: &Key) -> Option<Value> {
let idx = self.idx.load(Ordering::Relaxed) as u64;
let len = self.len as u64;
match name.as_str()? {
"index0" => Some(Value::from(idx)),
"index" => Some(Value::from(idx + 1)),
"length" => Some(Value::from(len)),
"revindex" => Some(Value::from(len.saturating_sub(idx))),
"revindex0" => Some(Value::from(len.saturating_sub(idx).saturating_sub(1))),
"first" => Some(Value::from(idx == 0)),
"last" => Some(Value::from(len == 0 || idx == len - 1)),
"depth" => Some(Value::from(self.depth + 1)),
"depth0" => Some(Value::from(self.depth)),
_ => None,
}
}
} |
What I'm observing is that adding a If there's some internal benefit to having only a single trait (though my instinct is that two traits would also simplify things internally), you could still unify both internally either via an Alternatively, if an |
Yes and no. Generally the benefit this actually has is that it would permit lazy loading of either structs/maps or sequences. You could place an object in the scope and only the first time it's interacted with would it turn into the target value (for as long that value can be represented by However because maps and sequences do not behave the same, there are various different places where the differences matter.
The original intention of the
That's only possible if the iteration behavior of the object becomes part of the object's interface. An object that behaves like a map iterates over the keys, an object that behaves like a sequence iterates over the values as an example. Likewise serializing the thing to JSON also produces obviously different representations of the final structure. I have implemented different forms of this as an experiment and I'm not particularly happy with any of these but I'm exploring around. One thing I have been playing with that ensures that you cannot accidentally be two things at once is this: enum ObjectBehavior<'a> {
Seq(&'a dyn Seq),
Struct(&'a dyn Struct),
}
trait Object {
fn behavior(&self) -> ObjectBehavior<'_>;
fn call_method(&self, state: &State, name: &str, args: &[Value]) -> Result<Value, Error> { ... }
fn call(&self, state: &State, args: &[Value]) -> Result<Value, Error> { ... }
}
trait Seq {
fn get(&self, idx: usize) -> Option<Value>;
fn len(&self) -> usize;
}
trait Struct {
fn get(&self, key: &str) -> Option<Value>;
fn keys(&self) -> Box<dyn Iterator<Item = &str> + '_>;
} It requires implementing two traits but beyond that it's quite nice: struct MySeq;
impl Object for MySeq {
fn behavior(&self) -> ObjectBehavior<'_> {
ObjectBehavior::Seq(self)
}
}
impl Seq for MySeq {
fn get(&self, idx: usize) -> Option<Value> {
if idx < self.len() {
Some(Value::from(idx * idx))
} else {
None
}
}
fn len(&self) -> usize {
10
}
} |
Using
Object
cut down the running time of my application by 25% by allowing me to remove clones in favor of using proxy objects. From profiling, I can see that a similar possibility for optimization exists for sequences.At present, if I have an existing sequence of non-minijinja-values, I have no recourse but to clone the entire sequence, converting each value into a native
minijinja::Value
in the process:This is expensive, especially when the sequence is large. If instead there was a trait like
Object
for sequences, I could wrap the array in a proxy object like I do maps and save the cost:One option is to introduce another trait and
ValueRepr
kind, saySequence
, which looks a bit likeObject
:Alternatively,
Object
can be generalized to be useful for sequence-like objects as well. One approach for this option is to add the sameget()
andrange()
methods above toObject
. Another is to make the existingget_attr()
andattributes()
methods more versatile, allowing the former to take ausize
and the latter to emit arange()
.The text was updated successfully, but these errors were encountered: