Tea
Posted on November 20, 2020
Perhaps a month ago, I teased a coming post about bracket/delimiter/begin-end patterns in C#...
What is a bracket pattern?
If you have used OpenGL, you may remember code looking like:
glBegin(GL_QUADS);
glColor3f(1.0f, 0.0f, 0.0f);
glVertex2f(-0.5f, -0.5f);
// ...
glEnd();
These patterns are not uncommon and they are, well - error prone. Ever accidentally left your front door open? That is how goes with pairing commands as above.
Bracket patterns in C-sharp
In C# you may leverage using
and IDisposable
. However this is on the heavy side, primarily for safely managing resources.
I review lightweight alternatives - looking at increasingly more powerful variants.
The basics
In the simplest case, we want a pattern equivalent to...
Begin();
DoSomething();
End();
A basic bracket pattern, then, is implemented as follows:
class Service{
public Func<T> With{ get{
Start();
return End;
}}
void Start(){ ... }
void End(T arg){ ... }
}
Enabling the following use:
With( DoSomething() );
Returning the With
function as a property allows running code both before and after running the enclosed command.
One use case is with functional graphic APIs (such as OpenGL, and others...); Unity, for instance, provides the following API(*):
(*) In places they now use IDisposable
BeginHorizontalGroup();
Label("Hello");
EndHorizontalGroup();
Which then becomes:
HorizontalGroup( Label("Hello") );
And the pattern implementation:
public static class GUI{
public static Func<object> With{ get{
BeginHorizontalGroup();
return End;
}}
void End(object arg) => EndHorizontalGroup();
}
As you see we are not doing any with the argument - our purpose here is only 'framing' the Label()
command. Which brings us to the one constraint these patterns are sharing: Label()
(and likewise framed commands) cannot return void
.
Although I did use a static
class this is not required; so, assuming the target API does not use globals either, thread safety may be preserved.
A common use case for bracket patterns is when building hierarchies and this may help designing articulate alternatives to the builder pattern.
In its most simple form the bracket pattern only supports one argument. What if (as makes sense with a horizontal group) we wanted to combine several commands? Then, use a delegate:
public class Service{
public EndFunc With{ get{
Start();
return End;
}}
void Start(){ ... }
delegate void EndFunc(params T[] args);
void End(params T[] args){ ... }
}
The reason we need a delegate here is var-args (...
) are not supported by any variant of System.Func<...>
.
Back to the 'horizontal group' example, the above then enables the following syntax:
HorizontalGroup(
Label("Hello"),
Button("Tap to enter"),
);
If you go back to the OpenGL example, note how the glBegin(GL_QUADS)
function allowed arguments. In the next session I'll be looking at a handy variant which cleanly separates arguments to the begin
function from the enclosed items.
Var-arg forms allocate memory; often the impact is negligible; when it's not there is an alternative, and I'll also review this in my next posting.
Happy Coding ~
Photo by Dan-Cristian Pădureț on Unsplash
Posted on November 20, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.