-
Notifications
You must be signed in to change notification settings - Fork 116
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
Implement nested composition of FromLuaMulti #287
Conversation
Signed-off-by: Tin <tin.svagelj@live.com>
Signed-off-by: Tin <tin.svagelj@live.com>
Signed-off-by: Tin <tin.svagelj@live.com>
Signed-off-by: Tin <tin.svagelj@live.com>
Hi, I wonder if this is maybe a bit too much extra complexity for what seems like is an unusual case, plus I worry that there's a lot of scope for confusion when some type consumes more (or fewer) values than the user expected. In my experience it can sometimes be tricky debugging issues where the automatic argument conversions fail (your function ends up not being called), and this feels like they'd have more ways to go wrong. Do you gain much from this modified trait being in |
I register 50+ methods which all take A simple case here would add like ~5 more lines because I'd have to match against the tailing It also makes it very straightforward to implement functions with
In some cases I have 5 arguments all of which would have to be manually type checked. In some cases, 1-line expressions would have to be turned into 7 lines of code, so without this feature, bindings code would be about 50% larger. I also sometimes need to optionally capture either 2 (x & y) arguments or none. This change makes it very convenient to configure the number of consumed arguments in method signature instead of body which I find nicer, but same functionality could be achieved with proposed Implementation of
I could provide index of failed top-level argument Type (and corresponding Lua argument index) in the error message if you find it necessary, but from what I experimented with 1-level of nesting, the error messages produced from
Note that while I have written over 2000 LoC using this feature, I haven't tested it much yet. |
Old behavior works the same, this addition allows additionally passing types that specialize FromLuaMulti which would then cause the Table entries to be able to get collected from multiple consecutive values in table such as: { a, { b.a, b.b }, c, d, { e.a, e.b } } If `b` errors, error is on `b` not being T. Signed-off-by: Tin <tin.svagelj@live.com>
Signed-off-by: Tin <tin.svagelj@live.com>
Make Variadic<T::from_lua_multi> loop more explicit Signed-off-by: Tin <tin.svagelj@live.com>
|
Signed-off-by: Tin <tin.svagelj@live.com>
Signed-off-by: Tin <tin.svagelj@live.com>
This prevents a panic when count is bigger than length Signed-off-by: Tin <tin.svagelj@live.com>
Idea from: kyren/piccolo@24a2c16 Signed-off-by: Tin <tin.svagelj@live.com>
Sorry for the delay between replies! I can see that this is a very useful feature for some use cases! To recap, my current concerns are:
So I'd like to make sure we can balance the problems with the benefits. Some thoughts I've had (but not had the time to investigate properly):
pub struct FlexibleFromLuaMulti<T>(T);
... generated from macro for different sized tuples
impl<A:NewFromLuaMulti, B:NewFromLuaMulti> FromLuaMulti for FlexibleFromLuaMulti<(A, B)> {
fn from_lua_multi(...) {
let mut consumed = 0usize;
// delegate to NewFromLuaMulti::from_lua_multi(..., &mut consumed)
}
}
// using it
methods.add_method("addPoint", |_, this, FlexibleFromLuaMulti((point, color)): FlexibleFromLuaMulti<(FromLuaPoint, FromLuaColor)>| {
// do something with point and color parsed from LuaMultiValue
println!("Added point: {}, {}", point.x, point.y);
}); I guess a shorter name would probably be needed!
The (probably fatal, but maybe there's a creative solution) issue I can see here is that I don't think there's a way to do this without either forcing some users to implement both methods, or having an impl which implements neither which compiles but fails at runtime. I'm still thinking about this - I'd appreciate your thoughts. |
Right, a solution that's backwards compatible did cross my mind at some point but I didn't have time to implement it and half-way forgot about it, but it was something like: pub trait FromArgumentPack<'lua> {
// current FromLuaMulti::from_lua_multi
}
impl<'lua, M: FromLuaMulti<'lua>> FromArgumentPack for M {
// pass to multi without counter
}
// `impl for FromLua` is done though `impl FromLuaMulti for FromLua`
// use `FromArgumentPack` instead of `FromLuaMulti` in `impl_tuple` So it's similar to adding a function to I think that would:
Preventing I don't think the "added confusion when
That also leads to another point - My |
Hi,
Do you think it's still a lot of work to get to that from where you are now? I may find time to pitch in a bit. |
Shouldn't be a lot of work, just have to replace |
I tried doing it with the wrapper trait. The issue is the language lacks negative trait bounds so this solution can't work:
I tried replacing
That also answers the question:
Anything reading from I have a couple of ideas for some other approaches so I'll try those out as well. |
Signed-off-by: Tin <tin.svagelj@live.com>
Signed-off-by: Tin <tin.svagelj@live.com>
Signed-off-by: Tin <tin.svagelj@live.com>
src/value.rs
Outdated
/// if not enough values are given, conversions should assume that any | ||
/// missing values are nil. | ||
fn from_lua_multi(values: MultiValue<'lua>, lua: Context<'lua>) -> Result<Self> { | ||
Self::from_counted_multi(values, lua, &mut 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think adding a separate method like this is probably the cleanest solution that would cause least breakage in dependent code. It also makes the diff a lot more concise.
The only issue I can see is someone implementing FromLuaMulti
without overriding either method which would cause infinite recursion at runtime, but I think it's obvious that something has to be specified (and it's noted in the doc).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The benefit of having those methods call each other is that dependents only need to implement one, depending on whether they need to handle arguments non-exhaustively.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like having the option of implementing either, but I don't like the failure mode if you don't. :-(
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is the best solution possible atm. Alternatives are:
- depending on nightly,
- having to manually implement both methods, or
- the first solution which requires possibly a lot of updates to existing code base.
Signed-off-by: Tin <tin.svagelj@live.com>
@jugglerchris I think this is the final solution, just let me know whether you think there would be any benefit to passing the |
src/value.rs
Outdated
/// if not enough values are given, conversions should assume that any | ||
/// missing values are nil. | ||
fn from_lua_multi(values: MultiValue<'lua>, lua: Context<'lua>) -> Result<Self> { | ||
Self::from_counted_multi(values, lua, &mut 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like having the option of implementing either, but I don't like the failure mode if you don't. :-(
src/conversion.rs
Outdated
let mut result = Vec::new(); | ||
while values.len() > 0 { | ||
let mut consumed = 0; | ||
match T::from_counted_multi(values.clone(), lua, &mut consumed) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cloning the values each time seems like a problem - it means means converting the list is now O(n^2).
I'm thinking aloud (you've looked at this a lot more than I have recently), but it feels like passing &mut values
instead of by-value would solve that and also make the consumed
count unnecessary as from_counted_multi
could just drop_front(...)
(or maybe consume(n)
) instead.
The downside is that the function could do other weird things like replace it with a different list.
Perhaps a narrower wrapper MultiValueView
or something which can only consume items. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The main drawback with that approach is that implementations have to manually juggle values (revert them when testing fails). Fallible
could do a clone and revert it on error but it complicates the implementation for most other structures.
I tried adding a wrapper as well at some point but it required adding lifetimes to all the traits/functions that deal with FromLuaMulti
conversion which felt like it complicates code too much. Though I do think an iterator with "revert last argument" function and "list skipped types" function would be ideal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that adding extra lifetimes to traits would be awkward, and make updates from previous versions difficult.
But I think the quadratic runtime for long (even non-multi-value) argument lists is also a problem.
That quadratic issue is only because multiple arguments can have a crack a (nearly) the same list, and wouldn't be a problem with the old "take all" version. Maybe the new counted method can take a different type which does behave as an iterator. Perhaps with a "peek next" rather than "revert pop"?
src/multi.rs
Outdated
) -> Result<Self> { | ||
match T::from_counted_multi(values, lua, consumed) { | ||
Ok(it) => { | ||
*consumed += 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this increment for? I would expect the T::from_counted_multi()
would already have done this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, that's an error. Didn't test this latest version as much.
src/value.rs
Outdated
fn from_counted_multi( | ||
values: MultiValue<'lua>, | ||
lua: Context<'lua>, | ||
consumed: &mut usize, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than incrementing consumed, should it just start at 0 and the implementer just writes the number consumed, rather than incrementing a value they're not interested in?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That could also work but it makes the code more complicated as consumed would have to be constructed from scratch before each call. In case of nested arguments that are composed out of several others, that's a lot more room for error.
While reading your review, I thought about maybe removing the added method and changing the existing fn from_lua_multi(values: &mut MultiValue<'lua>, lua: Context<'lua>) -> Result<Self> which is a mix of one of your suggestions and the initial version. It's good because:
but the downsides are:
I think we've covered all the different ways this could be implemented currently, so it's up to you to pick which stable tradeoffs you find acceptable. I was thinking about adding a feature for specialization but it's not needed if a breaking change is made and maintaining the additional code would be more complicated. I think this last suggestion to change |
Sorry again for slow replies!
I think changing the
I don't see any reason to change |
Signed-off-by: Tin Švagelj <tin.svagelj@live.com>
I added some small utility functions to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking great, thanks! I just noticed one out of date comment, otherwise I think this is good to merge. Thanks for your patience!
Signed-off-by: Tin Švagelj <tin.svagelj@live.com>
Give me a few days to update my binding code and maybe add some tests. There have been a few minor semantic changes I did since I last properly tested the entire modified surface area so I'd just like to make sure stuff like |
Signed-off-by: Tin Švagelj <tin.svagelj@live.com>
@jugglerchris Tested it, it seems to work so it's green flag from me as well. I added a The implementation with a counter reference was a bit simpler to implement (less code), but I see how it could've been confusing for people. Sorry it took me this much time, I had a lot of colloquiums and seminars at the uni I had to prepare for all at once and work. |
Thanks for the PR! I would recommend to swith your project to mlua and open the issue/discussion about this PR in the mlua repo to discuss possible options. mlua uses a modified version of |
This PR adds ability to compose
FromLuaMulti
values which makes it convenient to add functions which consume arguments of different kinds.This places an additional requirement on user implementing FromLuaMulti to mutate
consumed
argument with the number of arguments that were consumed from provided remaining arguments.I'm not too happy with complexity added by this feature, but it significantly simplifies my workflow in a case where I have to consume arguments in form
(&[(f32, f32)], bool)
and the tuple is supposed to be flattened. The main place that's affected by this additional argument isimpl_tuples!
trait.&mut usize
argument instead of return type as I think that's more convenient for implementers, as it allows gradual mutation of the usage counter instead of having to provide their own. It also provides argument location (didn't test that though).Usage
Expanded argument semantics
I'll express these as regexes where each Lua value is represented by a different letter. Groups (
bc
) represent Rust types which implementFromLuaMulti
directly, rest if flatFromLua
Rust types (a
&d
)FromLua
: Stays the same, blanket implementation ofFromLuaMulti
now updates theconsumed
by 1 if conversion succeeds or returns an error otherwise.Option<FromLuaMulti>
: works likea(bc)?d
regex, error is still ond
ifbc
isn't captured andd
can't be converted.Variadic<FromLuaMulti>
: works likea(bc)*?d
regex, error is ond
ifbc
isn't (i.e. is partially) captured andd
can't be converted.Vec<FromLuaMulti>
: works likea{(bc)*}d
regex, error is ond
.Variadic<FromLuaMulti>
onlybc
list has to be wrapped in aTable
.Breaking changes
FromLuaMulti::from_lua_multi
takes an additionalconsumed: &mut usize
argument.Memory
Currently, every$n$ arguments could temporarily do $(n - 1) \times 120B$ additional allocations on heap (due to $\sim32\text{B}$ ). That should fast path on most allocators due to size, but clone should probably be avoided if possible, or Cow used internally by
FromLuaMulti
is provided with a clone ofLuaMultiValue
. Given that the size ofValue
isusize
and max supported number of arguments is 16, this means that in worst case processingLuaMultiValue
being backed byVec
). That's a very degenerate case where a user specifies 16Option<Inconstructible>
types inadd_method
, in most cases it will do far less (LuaMultiValue
(didn't feel comfortable changing that).