digitalmars.D - Nifty chaining
- Steven Schveighoffer (68/68) Oct 04 2010 One of the warts of dcollections' API is this model:
- Peter Alexander (8/8) Oct 04 2010 I must be missing something, because I don't really see the need
- Steven Schveighoffer (21/29) Oct 04 2010 This works also. It depends on your preference for style.
- Peter Alexander (10/22) Oct 04 2010 But if add just returns the delta rather than the reference then
- Steven Schveighoffer (25/47) Oct 04 2010 I think it does work, and is much better than my example of with. There...
- Steven Schveighoffer (28/34) Oct 04 2010 Ran into a huge snag here. Because the functions being emulated are not...
One of the warts of dcollections' API is this model: class C(V) { C add(V v, out bool wasAdded) C add(V v) } So you have the ability to chain calls to C via: c.add(1).add(2); And it still supports the ability to determine if the values were added. But the API is clunky when you *do* want to know what was added, you have to pre-declare your variables, plus, when you are chaining, do you really care if each individual element was added, or do you want to know the effect of the entire expression? In addition, I have various different add overloads, one of which takes a dynamic array. I had wondered, wouldn't it be nice to just have this: C add(V[] elems...) and that would replace add(V v) as well. But I ran into a snag, I can't replace this function: C add(V[] elems, out uint numAdded) because what if V is uint? I thought of this idea: What I really want is the ability to chain calls, but also get the difference in length. So I built this struct, and it actually works: struct Chainer(T) { T t; private size_t origlen; this(T t) { this.t = t; this.origlen = t.length; } Chainer opDispatch(string fn, Args...)(Args args) if (is(typeof(mixin("t." ~ fn ~ "(args)")) == T)) { mixin("t." ~ fn ~ "(args);"); return this; } property int delta() const {return cast(int)(t.length - origlen);} } Chainer!T chain(T)(T t) { return Chainer!T(t); } So here we have a struct that allows you to chain, *plus* allows you to get at the returned delta in length. So you would use it like this: auto numAdded = chain(mycollection).add(1,2,3).add(4,5,6,7).delta; I originally wanted to just have each function that wanted to use chain calling return a Chainer!(typeof(this)), so you would use it like: auto numAdded = mycollection.add(1,2,3).add(4,5,6,7).delta; but this doesn't work for covariance. And I use covariance everywhere to allow chaining no matter what the derived type is. Although all dcollections classes are final, I use interfaces, and those could not be covariant (e.g. cannot implicitly cast Chainer!LinkList to Chainer!List). I also originally wanted to allow implicit casting of Chainer!T to int which would return the given delta, but this doesn't work. Adding 'alias delta this;' results in the error: Error: no property 'add' for type 'int' Which seems to suggest that the compiler will not try to use opDispatch when an alias this is present. Does that sound like a bug or expected behavior? Anyhow, the next version of dcollections will likely have these features. I wonder if a more generic "Chainer" type could be useful in Phobos. Basically, one which either accumulates some data on each chained call, or which determines a delta at the end. -Steve
Oct 04 2010
I must be missing something, because I don't really see the need for this at all. Why not just return the number of items added, and forget about chaining? If you're worried about having to type the container's identifier in over and over again, just use with(c) {...}, and if you want to chain together multiple different ranges, just use std.range.chain. Is there any use case where these don't suffice?
Oct 04 2010
On Mon, 04 Oct 2010 11:10:53 -0400, Peter Alexander <peter.alexander.au gmail.com> wrote:I must be missing something, because I don't really see the need for this at all. Why not just return the number of items added, and forget about chaining? If you're worried about having to type the container's identifier in over and over again, just use with(c) {...}, and if you want to chain together multiple different ranges, just use std.range.chain.This works also. It depends on your preference for style. I personally find this much more pleasant: auto numAdded = lengthChain(a).add(1,2,3).add(b).add(c[1..3]).delta; than: int numAdded = void; with(a) { auto olength = length; add(1,2,3); add(b); add(c[1..3]); numAdded = length - olength; } But you might not.Is there any use case where these don't suffice?Achieving it all with a single expression. You might not care whether that's possible, but I personally like it better. Whether it achieves much more than style points, I don't know. I'm guessing it might add a bit of bloat. -Steve
Oct 04 2010
== Quote from Steven Schveighoffer (schveiguy yahoo.com)'s articleI personally find this much more pleasant: auto numAdded = lengthChain(a).add(1,2,3).add(b).add(c[1..3]).delta;than: int numAdded = void; with(a) { auto olength = length; add(1,2,3); add(b); add(c[1..3]); numAdded = length - olength; }But if add just returns the delta rather than the reference then it's just: with (a) numAdded = add(1,2,3) + add(b) + add(c[1..3]); // (I'm hoping 'with' works like this! Can't test here) or numAdded = a.add(chain([1,2,3], b, c[1..3]); (I'm assuming b was a range here, otherwise you could just add it to the end of the first expression).
Oct 04 2010
On Mon, 04 Oct 2010 11:28:53 -0400, Peter Alexander <peter.alexander.au gmail.com> wrote:== Quote from Steven Schveighoffer (schveiguy yahoo.com)'s articleI think it does work, and is much better than my example of with. There's still the issue of declaring numAdded before the with statement. I think because with is a statement, you can't really use it as an expression. Thanks for your example, I definitely had not thought of using with multiple times within an expression, it's a good idea.I personally find this much more pleasant: auto numAdded = lengthChain(a).add(1,2,3).add(b).add(c[1..3]).delta;than: int numAdded = void; with(a) { auto olength = length; add(1,2,3); add(b); add(c[1..3]); numAdded = length - olength; }But if add just returns the delta rather than the reference then it's just: with (a) numAdded = add(1,2,3) + add(b) + add(c[1..3]); // (I'm hoping 'with' works like this! Can't test here)or numAdded = a.add(chain([1,2,3], b, c[1..3]); (I'm assuming b was a range here, otherwise you could just add it to the end of the first expression).It's not, it's another collection (an Iterator!V interface). Here is another example from dcollections: Deque concat(List!(V) rhs) { return dup().add(rhs); } I believe without chaining, this would be: auto retval = dup(); dup.add(rhs); return retval; Not sure if with helps here or not, I don't use it often enough. There are some brevity benefits you get by being able to express complex code as a single expression. With does help, but can't in all situations. Again, it's a matter of preference. There's also the issue of shadowing. I find myself using the chaining much more than determining the number of elements added. So making that the default seems more useful. -Steve
Oct 04 2010
On Mon, 04 Oct 2010 09:52:21 -0400, Steven Schveighoffer <schveiguy yahoo.com> wrote:Chainer opDispatch(string fn, Args...)(Args args) if (is(typeof(mixin("t." ~ fn ~ "(args)")) == T)) { mixin("t." ~ fn ~ "(args);"); return this; }Ran into a huge snag here. Because the functions being emulated are not templates, they do not do IFTI. But opDispatch *does* do IFTI. The issue is with literals. Easily illustrated: void foo(ushort x) { } void wrappit(string fn, Args...)(Args args) { mixin(fn ~ "(args);"); } void main() { foo(1); // ok wrappit!"foo"(1); // fails } The problem is, IFTI sees the "1" and instantiates "int". But foo needs a ushort, and this doesn't work. So how to fix this? I have no idea. The "type" of a literal is a polysemous type. I wonder if there's some way the compiler could try an ordered list of types and see which one fits. i.e. try to instantiate with int, if that doesn't work, try long, short, ushort, ulong, uint, etc. Anything that "1" can be interpreted as. Or maybe it would be enough to just use the smallest type it could possibly use? Can this work? Would it take too long? I lament for the day we can perfectly wrap functions... -Steve
Oct 04 2010