www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Nifty chaining

reply "Steven Schveighoffer" <schveiguy yahoo.com> writes:
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
next sibling parent reply Peter Alexander <peter.alexander.au gmail.com> writes:
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
parent reply "Steven Schveighoffer" <schveiguy yahoo.com> writes:
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
parent reply Peter Alexander <peter.alexander.au gmail.com> writes:
== Quote from Steven Schveighoffer (schveiguy yahoo.com)'s article
 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).
Oct 04 2010
parent "Steven Schveighoffer" <schveiguy yahoo.com> writes:
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 article
 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)
I 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.
 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
prev sibling parent "Steven Schveighoffer" <schveiguy yahoo.com> writes:
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