www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Queuing up operations to run as a batch (example)

reply cy <dlang verge.info.tm> writes:
aka I finally found a use for mixin templates :p

When I can batch up operations and run them quicker all at once, 
I usually make a list of those operations and append to it in 
lieu of calling the operation. Then periodically flushing the 
list, and actually calling the operation. Database insertions, or 
disk writes, or network sends, or unit tests, or something like 
that. Lazily caching operations to all fire at once, basically. 
So, I wrote a thing to do that in D, and it actually works pretty 
well. Thought I'd share it.

Basically it works like this:

struct Foo {
   void direct_operation(int a, int b) { ... }
   void transaction(Callable)(Callable inside) {
     setup();
     scope(exit) takedown();
     inside();
   }
   mixin template Batch!(transaction,direct_operation) operation;
}

...

Foo foo;
foo.operation(1,2);
foo.operation(3,4);
foo.operation(5,6);
foo.operation.flush();
foo.operation(7,8);

And here's the actual code:

mixin template Batch(alias transaction, alias operation, size_t 
max = 0x10) {
	import std.typecons: Tuple, tuple;
	import std.traits: Parameters,ReturnType;
	import std.array: Appender;
	
	static assert(is(ReturnType!operation == void));
	alias Item = Tuple!(Parameters!operation);
	Appender!(Item[]) cache;
	void opCall(Parameters!operation args) {
		cache.put(tuple(args));
		if(cache.data.length > max)
			flush();
	}
	void flush() {
		if(cache.data.length == 0) return;
		scope(exit) cache.shrinkTo(0);
		transaction({
				foreach(ref args; cache.data) {
					operation(args.expand);
				}
			});
	}
	void completely(Handle)(Handle handle) {
		scope(exit) flush();
		handle();
	}
}

mixin template Batch(alias setup, alias operation, alias 
takedown, size_t max = 0x10) {
	void transaction(Callable)(Callable inside) {
		setup();
		scope(exit) takedown();
		inside();
	}
	mixin Batch!(transaction,operation,max);
}


unittest {
	import print: print;
	struct Foo {
		void batchable_operation(int a, int b) {
			print("operate on",a,b);
		}
		void setup() {
			print("setup for flushing");
		}
		void commit() {
			print("commit");
		}
		void opCall(int a, int b) {
			print("oops",a,b);
		}
		mixin Batch!(setup,
								 batchable_operation,
								 commit,
								 0x10) C;
	}

	Foo foo;

	foo.completely({
			for(int i=0;i<20;++i) {
				foo.C(i,i*2);
				// .C isn't needed... except when the struct implements the
				// same operation, in which case that's the default!
				foo(i,i*3);
				print("did we do it?",i);
			}
		});
	print("done");

	struct Bar {
		void transaction(Callable)(Callable inside) {
			print("beginning");
			scope(exit) print("ending");
			inside();
		}
		void batchable_operation(int a, int b) {
			print("bar on",a,b);
		}
		mixin Batch!(transaction,
								 batchable_operation) C;
	}

	Bar bar;
	bar.completely({
			for(int i=0;i<20;++i) {
				bar(i,i*2);
			}
		});
}
Aug 22 2016
parent cy <dlang verge.info.tm> writes:
And now I remember that people still use the stupid default of 8 
spaces per tab, and I can't specify to use 2 in the text.

mixin template Batch(alias transaction, alias operation, size_t 
max = 0x10) {
   import std.typecons: Tuple, tuple;
   import std.traits: Parameters,ReturnType;
   import std.array: Appender;

   static assert(is(ReturnType!operation == void));
   alias Item = Tuple!(Parameters!operation);
   Appender!(Item[]) cache;
   void opCall(Parameters!operation args) {
     cache.put(tuple(args));
     if(cache.data.length > max)
       flush();
   }
   void flush() {
     if(cache.data.length == 0) return;
     scope(exit) cache.shrinkTo(0);
     transaction({
         foreach(ref args; cache.data) {
           operation(args.expand);
         }
       });
   }
   void completely(Handle)(Handle handle) {
     scope(exit) flush();
     handle();
   }
}

mixin template Batch(alias setup, alias operation, alias 
takedown, size_t max = 0x10) {
   void transaction(Callable)(Callable inside) {
     setup();
     scope(exit) takedown();
     inside();
   }
   mixin Batch!(transaction,operation,max);
}


unittest {
   import print: print;
   struct Foo {
     void batchable_operation(int a, int b) {
       print("operate on",a,b);
     }
     void setup() {
       print("setup for flushing");
     }
     void commit() {
       print("commit");
     }
     void opCall(int a, int b) {
       print("oops",a,b);
     }
     mixin Batch!(setup,
                  batchable_operation,
                  commit,
                  0x10) C;
   }

   Foo foo;

   foo.completely({
       for(int i=0;i<20;++i) {
         foo.C(i,i*2);
         // .C isn't needed... except when the struct implements 
the
         // same operation, in which case that's the default!
         foo(i,i*3);
         print("did we do it?",i);
       }
     });
   print("done");

   struct Bar {
     void transaction(Callable)(Callable inside) {
       print("beginning");
       scope(exit) print("ending");
       inside();
     }
     void batchable_operation(int a, int b) {
       print("bar on",a,b);
     }
     mixin Batch!(transaction,
                  batchable_operation) C;
   }

   Bar bar;
   bar.completely({
       for(int i=0;i<20;++i) {
         bar(i,i*2);
       }
     });
}
Aug 22 2016