August 23, 2016
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);
			}
		});
}
August 23, 2016
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);
      }
    });
}