Out parameters are a useful language feature for multiple results, particularly when the language in question doesn't have tuples as a first-class feature. Even though Java omitted pass-by-reference (meaning both var
and out
in Delphi parlance), C# did not follow its lead and includes both parameter-passing semantics. And so it follows that the .NET method System.Int32.TryParse(String,Int32&) is possible in C#, but not in Java.
The API works well enough when you want to put the extra return value(s) into local variables or visible fields. What about times when you want to put the return value into a property, though? Since you can't (generally) take a reference to a property, you have to manually create a local variable, pass the local variable by reference, and then, in a separate statement, assign the variable to the property.
Anonymous methods can provide an "out" here, if you'll pardon the pun. By passing a setter instead of a mutable reference, you can escape from this constraint. Here's are two overloaded ReadLine
functions, one using the out
mechanism, the other using the setter pattern:
function ReadLine(const Prompt: string; out Dest: string): Boolean; overload; var line: string; begin Write(Prompt, '> '); Flush(Output); Readln(line); Result := line <> ''; if Result then Dest := line; end; function ReadLine(const Prompt: string; const Setter: TProc<string>): Boolean; overload; var line: string; begin Write(Prompt, '> '); Flush(Output); Readln(line); Result := line <> ''; if Result then Setter(line); end;
As can be seen, the code that uses setters versus out parameters is little different. (It would be more different in a language that requires definite assignment, like C#, as otherwise we would have to set Dest to some value in the case of no entry.)
The real difference is in the usage point of this idiom. (And don't forget, this is an idiom; it might look odd the first time you see it, but when you understand anonymous methods, or even just the idiom, it's a trivial pattern and easy to understand.) Here's the definition of a class with a couple of properties, and a couple of functions. The first uses the out
idiom, and the second uses the setter idiom:
type TFrob = class private FFoo: string; FBar: string; public property Foo: string read FFoo write FFoo; property Bar: string read FBar write FBar; end; // Using 'out' idiom - requires separate assignment statement function WorkWithFrob: Boolean; var frob: TFrob; temp: string; begin frob := TFrob.Create; try if not ReadLine('Give me Foo', temp) then Exit(False); frob.Foo := temp; if not ReadLine('Give me Bar', temp) then Exit(False); frob.Bar := temp; Writeln(Format('Your foo is %s and bar is %s', [frob.Foo, frob.Bar])); Result := True; finally frob.Free; end; end; // Using 'setter' idiom - can be done inline function WorkWithFrob2: Boolean; var frob: TFrob; begin frob := TFrob.Create; try if not ReadLine('Give me Foo', procedure(x: string) begin frob.Foo := x; end) or not ReadLine('Give me Bar', procedure(x: string) begin frob.Bar := x; end) then Exit(False); Writeln(Format('Your foo is %s and bar is %s', [frob.Foo, frob.Bar])); Result := True; finally frob.Free; end; end;
This idiom isn't useful very often, but when the right situation comes up, it's really quite nice to have, especially when you need persistent references to properties - e.g. when you need to store the reference in some structure somewhere, and update it with different values over time.
Viewed more generally, anonymous methods / method references can act as kinds of pipes that join up producers and consumers of data, with getters (TFunc<TResult>) for consumer-pull and setters (TProc<TArg>) for producer-push.
9 comments:
Does this work the same was as if the properties in the class had been given setter methods? ie: Given such a setter method, can you pass the setter method itself instead of a chunk of anonymous method code?
@Raymond: The idiom presented here is agnostic as to the implementation details of the property. In other words, it doesn't matter if the property directly references a field or uses getter and setter methods, or some mix of both.
Now, if the setter method is visible (e.g. you are writing code belonging to the class itself), and it is compatible with TProc<TArg> where TArg is the property type, then you could pass the setter method directly, yes.
But from the outside, you can't tell at the use-point whether the property is using a direct field access or a setter, and indeed you wouldn't want to know - it would be a violation of encapsulation. So, for code on the outside, you're best off writing the little anonymous method thunk.
A nice syntax improvement would be:
if not ReadLine('Give me Foo', |x: string| begin frob.Foo := x end)
or not ReadLine('Give me Bar', |x: string| begin frob.Bar := x end) then
Exit(False);
To complete Junior:
Or perhaps to allow the begin/end pair to be optional? As in if {cond} {statement}, for {cycle} {statement} in the same manner why should be compusory that the routine body to be between begin/end? IOW, if the routine has only one statement then begin/end can be omitted. So, we getting close to lambdas, isn't?
Just my 2c.
Junior, m. Th: A better syntax will have to wait until the compiler is better configured for type inference.
@Barry: Aha! Thanks Barry. Way to go, son, way to go! Of course, isn't necessary to remind you that having type inference is a much bigger deal than what we discuss here, and _just_IMHO_ type inference musn't be avoided because 'opens the possibility for nasty programming errors' as, perhaps, a Pascal purist might say.
Just my2c,
m. Th.
FINALLY! A solution to having to create different blocks of "getData" and "setData" methods! This is common when using, say, ini or regini for data storage.
procedure getData(obj : TMyObj);
begin
obj.var1 := ReadStr('xx','var1','');
obj.var2 := ReadInt('xx', 'var2',0);
end;
procedure setData(obj : TMyObj);
begin
WriteStr('xx','var1',obj.var1);
WriteInt('xx','var2',obj.var2);
end;
If var1 and var2 are set as properties, you can't make both signatures the same, eg.,
ReadStr('xx','var1',obj.var1)
and
WriteStr('xx','var1',obj.var1)
But using closures the way you're illustrating, the whole approach can be made much more elegant and far simpler. Let the compiler do this work rather than the code writer!
-David
A nice one Barry!
I just wish you would show the way by providing a (new) standard formatting to write anons instead of compacted 1 liners.
Keeping somehow the standard routine layout helps a lot if you need step by step debugging (helps also in CPU view). Anonymous methods must not become associated with unreadability... Just my 2c.
maybe writing like:
if not ReadLine('Give me Foo',
procedure(x: string)
begin
frob.Foo := x;
end) or
Unfortunately, I see the indentation is gone...
Post a Comment