Translating intentions and mechanisms
Before I get into today's blogging, a quick note about my recent post on How To Not Get A Question Answered. That was certainly not intended to be fishing for compliments or chiding people for never acknowledging help ten years ago; that said, I appreciate both. Thanks to everyone who made thoughtful comments.
Moving on; a theme that seems to be coming up over and over again in my recent technical conversations is that of intention vs. mechanism. We have tried hard in C# 3.0 to make a language where there is a good balance between the code reading as a declaration of what meaning you intend the code to represent, and reading as a list of imperative instructions specifying a mechanism which achieves those intentions. I've been accumulating anecdotes about this tension between representing intentions vs. mechanisms; expect this to be a recurring theme in the blog for the next while.
Here's a question I got recently which speaks to this tension with regards to the subject of porting code from one language to another:
I have some C++ code with a macro in it. A typical usage looks like this:
TRY_WAIT_OP(Execute());
This macro expands to code similar to this:
for(int i=0; i<10; i++) {
if (Execute()) break;
Sleep(i*1000);
}We are translating this code into C#, but C# does not have a #define directive. How do I do textual replacement of code in C#?
Once more, we have someone looking for a thin metal ruler. There's a problem -- represent the meaning of a common operation in a programming language. C++ provides a solution mechanism -- using preprocessor-based metaprogramming to create a nonstandard control flow primitive. The natural tendency when doing a translation is to find the identical mechanism in the new language, and then translate the code to use that mechanism. But what if there is no such mechanism?
In that case, you've got to translate the intentions, which after all, is what you are trying to translate in the first place. Presumably the new code is intended to do the same thing as the old code. Translating the mechanisms is just a particularly easy road to translating the intentions. What is the meaning of this macro?
Clearly TRY_WAIT_OP means "execute some arbitrary code that returns a Boolean. If it returns true, you're done. If it returns false, wait some amount of time and try again, up to ten times".
Now think about how you would write code from scratch that implemented those intentions in C#. Don't think at all about how it was written in C++. Your goal here is to solve the same problem using a different tool, so don't use the same techniques that you used for the other tool if they're not appropriate. Use the techniques that are appropriate for this tool.
The way I would write that in C# is to write a method that takes as its argument some arbitrary code that returns a Boolean. "Arbitrary code" is represented in C# by a delegate. We can do a bit better than the macro while we're at it, and return a success code:
private static bool AttemptMultiple(Func<bool> action) {
Debug.Assert(action != null);
const int maxAttempts = 10;
const int delay = 1000;
for (int attempt = 0 ; attempt < maxAttempts ; ++attempt) {
if (action()) return true;
Sleep(attempt * delay);
}
return false;
}
Lambda expression syntax gives us a nice way to do the call:
AttemptMultiple(()=>Execute());
A code porting project is a good opportunity to review the design fundamentals:
- Why 10 retrys? Should this be a parameter to AttemptMultiple?
- Why wait 1000 milliseconds instead of some other delay? Should this also be a parameter?
- Why is the increasing delay linear rather than constant, geometric, etc? Should the delay strategy be a parameter?
The above questions are good but they rather miss the point. The important question is: is this functionality even a good idea in the first place?
This last point is key. The "try it, fail, wait, try again" strategy is in general a dangerous one because it does not compose well with itself. Consider the following:
bool SendPackets() { ... if (!AttemptMultiple(()=>{ ... })) return false; ... }
bool TalkToSocket() { ... if (!AttemptMultiple(()=>SendPackets())) return false; ... }
bool SendData() { ... if (!AttemptMultiple(()=>TalkToSocket())) return false;... }
void HandleCommand() { ... if (command == SendData && !AttemptMultiple(()=>SendData())) ReportErrorToUser();... }
Now suppose that the user has a bad network card and SendPackets is always going to fail. If you look at any one of those lines, it looks like the attempt is being made ten times and will take a maximum of about one minute. In fact, the attempt to send the packet is made ten thousand times and will not report the error to the user for about a week.
Usually the right thing to do when something fails is to go into a failure state immediately. Tell the user that something failed and let them decide when and if to retry it.
What are some examples of a poor mismatch between intentions and mechanisms that you guys have seen? I'm interested in stories about:
- mechanisms that subtly did not implement the intentions of the programmer
- situations where the intention of the code was completely obscured by the mechanisms. How did you make the code better reflect the intentions?
- situations where the mechanism of the code was important, but obscured by unnecessary emphasis on representing the intention. What were the negative consequences of obscuring the mechanism behind some abstraction?
Thanks!
Comments
Anonymous
March 25, 2008
The comment has been removedAnonymous
March 25, 2008
That would work in this case. But what if the code inside the macro took an argument?Anonymous
March 25, 2008
Relevant to the "automatic retry" discussion: http://blogs.msdn.com/oldnewthing/archive/2005/11/07/489807.aspxAnonymous
March 25, 2008
In my case, it's dealing with atronomical instruments that occasionally misbehave in an automated image acquisition system. The devices operate below a standardized driver layer. And the real problem is political - users want me to do automatic retries instead of them having to fix the problem (or have their instrument supplier fix it!). It's very difficult to explain why I refuse to put in "recovery" code for their flaky device. It needs to be in the driver if anywhere, and I say "nowhere"; talk to the people to whom you paid money for the instrument. Tell them to make it work reliably. Anyway, the reasons for avoiding retries are (1) avoiding damage to equipment, and (2) avoiding bloating my software with device-dependent code, and simultaneously defeating the whole purpose of drivers. "No software victories over hardware!"Anonymous
March 25, 2008
Great post, Eric. Keepem' coming.Anonymous
March 25, 2008
The whole standard SQL synthax looks like an example of the third case (too much emphasis in intention). I think the Linq synthax is better, not only for allowing intellisense but for expressing the mechanism (order) the operations should be done. When will we have a Microsoft Linq Server that takes ExpressionTrees insead of plain SQL :PAnonymous
March 27, 2008
Deadlocks is about the only area I've come across where this technique is useful.Anonymous
March 28, 2008
I imagine everyone here already knows about it but www.TheDailyWTF.com is chock FULL of examples of mismatches between intentions and mechanisms.Anonymous
March 28, 2008
The comment has been removedAnonymous
March 30, 2008
LINQ in Portuguese ( Direct ) http://www.linqpad.net Eric Lippert Why Can't I Access A Protected MemberAnonymous
March 30, 2008
Welcome to the forty-second issue of Community Convergence. The last few weeks have been a busy timeAnonymous
April 17, 2008
Hey Eric (and the others), I' m a Belgian banker who programs for fun (over ten years ago I did this for my job). i surely agree with the translating manner, but I have some doubt with the statement: "Tell the user that something failed and let them decide when and if to retry it." I explain: on my home-network we have now and then some transmission problems. I have written a small program to make a website with photos en I have to transfer this websites to my webspace. The transfer of a website takes in certain cases an hour or more, so I want my computer to upload the website unattended .... but I did NOT find a (free) FTP-program that I could tell to retry for about a minute or two when something goes wrong (they all have read your statement?). So I have written a simple FTP-program myself with the AttemptMultiple-approach ... and it works fine! (Sorry for the errors in my writing English, my mother-language is Dutch.)