I want to implement "do once" pattern that allows me to avoid writing 3 things:
- declaring var first = true
- if(first) Do(...) statement inside repeated block of code
- first = false assignment inside repeated block of code
I also want to avoid workarounds like these:
- manually maintaining and passing unique identification into Do function
- defining once context variable multiple times
So my code should look as simple as this:
using(var once = new Once())
foreach(var it in new[]{1,2,3}){
once.Do(()=>Console.Write("It should write once and only once"));
Console.Write("It should write 3 times");
foreach(var it2 in new[]{4,5}){
once.Do(()=>Console.Write("Inner loop should write once and only once"));
Console.Write("It should write 6 times");
}
}
or this:
using(var once = new Once())
foreach(var it in new[]{1,2,3}){
once.Do(()=>Console.Write("It should write once and only once"));
Console.Write("It should write 3 times");
once.Do(()=>Console.Write("It should write once, but only after first instance (out of 3) of previous write."));
foreach(var it2 in new[]{4,5}){
once.Do(()=>Console.Write("Inner loop should write once and only once"));
Console.Write("It should write 6 times");
once.Do(()=>Console.Write("It should write once, but only after first instance (out of 6) of previous write."));
}
Console.Write("Repeated ending should appear 3 times");
}
If I use ObjectIDGenerator, it does not solve my problem because it gives different Id for Action act for every call in this implementation implementation:
public class Once : IDisposable{
HashSet<long> passed;
static ObjectIDGenerator idgen = new ObjectIDGenerator();
public Once(){
passed = passed.New();
}
public bool Do(Action act){
if(act != null){
bool firstTime;
var id = idgen.GetId(act,out firstTime);
if(!passed.Contains(id)){
act();
passed.Add(id);
return true;
}
else
return false;
}
else
return false;
}
void IDisposable.Dispose() {
passed.Clear();
}
}
How to get unique id of passed lambda function ? I think it can be done by traversing it as Expression Tree and calculating hash or otherwise serializing it into something that can be placed into HashSet.
But I would prefer if it was possible to find file name and line number in source code that defines that lambda function and use it as id. That would easily solve problem of different places of definition having different unique ids even if definition was copy&pasted.
I guess one way would be to use ObjectIDGenerator for Expression Tree object that represents this lambda function. Would ObjectIDGenerator return same id for that Expression Tree ?
Another example: how to implement Once class and include nested loop variable it2 into once.Do invokation so that it would be called only twice - once for it2 = 4 and once for it2 = 5:
using(var once = new Once())
foreach(var it in new[]{1,2,3}){
once.Do(()=>Console.Write("It should write once and only once"));
Console.Write("It should write 3 times");
foreach(var it2 in new[]{4,5}){
once.Do(()=>Console.Write("Inner loop should write twice and only twice: {0}", it2));
Console.Write("It should write 6 times");
}
}
Another example: how to implement Once class and include outer loop variable it into once.Do invokation so that it would be called only 3 times - once for it = 1, once for it = 2 and once for it=3:
using(var once = new Once())
foreach(var it in new[]{1,2,3}){
once.Do(()=>Console.Write("It should write once and only once"));
Console.Write("It should write 3 times");
foreach(var it2 in new[]{4,5}){
once.Do(()=>Console.Write("Inner loop should write 3 times and only 3 times: {0}", it));
Console.Write("It should write 6 times");
}
}
Another clarification: If there is second lambda function that is defined somewhere else in my code, I want it to have different id even if it's a copy&paste of first lambda and has identical implementation.
using(var once = new Once())
foreach(var it in new[]{1,2,3}){
once.Do(()=>Console.Write("It should write twice because it's defined in different lines of code"));
once.Do(()=>Console.Write("It should write twice because it's defined in different lines of code"));
Console.Write("It should write 3 times");
}
Now thinking about it, in an ideal solution I would exclude from id anything that is passed as one of explicit parameters like this (x,y,z,...)=>...
and include values of any captured context variables referenced by that lambda function. So
using(var once = new Once())
foreach(var it in new[]{1,2,3}){
once.Do((arg)=>Console.Write("It should write once {0}",arg));
once.Do(()=>Console.Write("It should write 3 times {0}",it));
Console.Write("It should write 3 times");
}
Or may be inversion is better:
using(var once = new Once())
foreach(var it in new[]{1,2,3}){
once.Do(()=>Console.Write("It should write once {0}",it));
once.Do((arg)=>Console.Write("It should write 3 times {0}",arg));
Console.Write("It should write 3 times");
}
Either way the goal of last 2 examples is to show how be able to control cleanly what is included into determination of uniqueness and what is not.
Addressing solution from Jon here is another clarification:
I want to keep definition of once and non-once actions in the same sequence as if they were all non-once, so that order of appearance of a,b,c in my source code does not have to be changed if I decide to write b only once or not:
using(var once = new Once())
foreach(var it in new[]{1,2,3}){
Console.Write("a");
Console.Write("b");
Console.Write("c");
}
does not have to be changed:
using(var once = new Once())
foreach(var it in new[]{1,2,3}){
Console.Write("a");
once.Do(()=>Console.Write("b"));
Console.Write("c");
}
Realistic example - imagine some table with many possible amount fields, imagine that I cannot generate script and execute it in one batch (which is the case with Azure and probably other cloud databases), also imagine that we also define AfterFirst method in our class Once:
using(var tblOnce = new Once())
foreach(var tbl in db.Tables)
using(var fldOnce = new Once())
foreach(var fld in tbl.Fields){
fldOnce.Do( ()=>conn.Exec(" CREATE TABLE {0}({1} {2})",tbl.Name, fld.Name, fld.SqlType));
if(fld.Name.EndsWith("Amount"))
fldOnce.Do( ()=>conn.Exec(" ALTER TABLE {0} ADD Total money", tbl.Name));
fldOnce.AfterFirst(()=>conn.Exec(" ALTER TABLE {0} ADD {1} {2}", tbl.Name, fld.Name, fld.SqlType));
if(fld.PrimaryKey)
fldOnce.Do( ()=>conn.Exec(" ALTER TABLE {0} ADD CONSTRAINT PK_{0}_{1} PRIMARY KEY CLUSTERED({1})", tbl.Name, fld.Name));
fldOnce.Do(()=>
tblOnce.Do( ()=>conn.Exec(" CREATE TABLE Tables (name varchar(50))"));
conn.Exec(" INSERT tables (name) select " + tbl.Name);
);
}