Streamlining and Boilerplate Reduction

The theme of streamlining code and reducing boilerplate extends through the entire ModVB agenda. In the previous section I talked about how improvements in type inference can do this and in future sections I’ll show more examples that also align strongly with broader themes. This section highlights a few investments that don’t align with other themes but rather are purely planned to delete characters from code while still preserving VBs intuitive and self-descriptive nature.

Immersive Files (incl. top-level code)

The feature in this section with the farthest reaching impact potential is what I have recently dubbed Immersive Files. This includes “top-level code” which appears in many other languages, but as is often the case, ModVB extends this concept much further, so don’t skip the details on this one.

Of course, it all began with top-level code and trying to get back to the simplicity I started with in QBasic—that a program could be one or more executable lines of code without all the boilerplate of a `Main` method or a module to contain it

FirstProgram.vb

Console.WriteLine("Hello, World!")

SecondProgram.vb

' This is an entire program.
Imports System.Console

Function Prompt(message As String) As String
    WriteLine(message)
    Return ReadLine()
End Function

Let name = Prompt("What is your name?")

WriteLine($"Hello, {name}​.")

These are great demos for educational purposes and there was much discussion of these “simple programs” and wanting to bring scripting and interactive experiences to VB and C# like what F# has had forever. And creating as minimal an on-ramp for new developers is of course critically important but the immersive files feature is designed such that they can have value for any developer or project regardless of skill level.

If you’re an experienced VB developer, when you read the above simple programs you probably came to some quick conclusions about what was going on like that the executable statements run in a Sub Main or that the function Prompt was being declared in an implicit module, which are certainly true of how I imagined those files would be compiled but the great thing about immersive files is that they can be useful for an open-ended number of immersive experiences with completely different compilation (maybe) and execution models. More explicitly,

  • The statements will end up in a method but it doesn’t have to be a Main method nor does it have to be Shared and it could be an Async or even Iterator method.
  • Any non-type declarations will end up bundled in a type, whose name is inferred from the filename, but that type doesn’t have to be a module or even a class. This type can be Partial.

Instead of me as a language designer making these decisions, I have designed the feature so that these decisions can be made by VB developers and communicated to the compiler through attribution from a base class that a file may derive from explicitly through an Inherits statement or implicitly through configuration.

Let’s look at some examples…


Starfield.vb

buffer.Clear(Color.Black)

' Draw 10 yellow 5x5 squares randomly across a 600x400 screen.
For i = 1 To 10
    buffer.FillRectangle(Brushes.Yellow, Rnd() * 600, Rnd() * 400, 5, 5)
Next

In this video I recorded, I have defined a base class called GameLoop that is attributed so that when an immersive file inherits it, any of its top-level statements go into an Execute method that’s run on every frame and can draw to a graphics buffer. This allows the developer to be “immersed” in the animation loop of this starfield.


Index.vbxhtml

<?xml version="1.0" encoding="UTF-8"?>
<html>
  <head>
    <title>Welcome to my site!</title>
  </head>
  <body>
    <h1><{ ViewData!Message }></h1>
    <h2>This is a header.</h2>
    <p>Today is <{ Date.Today.ToString("MMM d, yyyy") }>.</p>
    
    <a href="/">Home</a>
    <a href="/home/about">About</a>
    <a href="/home/contact">Contact</a>
  </body>
</html>

And in this video, I use a base class that puts top-level statements inside a Render method that sends HTML from an ASP.NET Core MVC controller to a browser. This example takes advantage of a top-level expression that’s a single large XML literal which is the return value of the top-level method that the base class then calls when rendering the page to the response stream. This is an extreme though common example where the primary logic of an entire type, in this case a View class, is entirely or almost entirely restricted to a single function—computing the result markup. This feature allows the developer to be “immersed” in that all-consuming purpose.


I will use this same technique in the future to enable VB to easily utilize several of .NET’s many popular XAML-based UI frameworks. But in these case the top-level expression will be a XAML literal (coming in Section 4) forming the body of the implicit InitializeComponent of a Window, UserControl, or ContentPage from these frameworks. This approach will bring cross-platform and mobile UI development to VB, without requiring any support from UI framework authors.


Supporting top-level LINQ query expressions (which could be compiled or simply interpreted in the IDE) will allow the creation of immersive interactive live data analysis experiences like what you get in SQL Server Management Studio, LINQPad, or other tools for data science (expect a demo one day).


Or, a VB.NET Script file for automation and management of Windows or other operating systems…


But even beyond all that fancy stuff, what if I’m just tired of Class statements?

Hear me out.

In VB6, .frm files were forms I didn’t need an explicit Class statement. The name of the form always matched the name of the file. And in VB.NET we lost that “whole file” immersive experience in exchange for the ability to put multiple types in a single file, which is great, but what if we could have it all?

Form1.vb

Sub Button1_Click() Handles Button1.Click
    MsgBox("Hi")
End Sub

Sub Button2_Click() Handles Button2.Click
    MsgBox("Bye")
End Sub

Combined with VB’s already great experiences of project root namespaces, and project-level imports, that file is looking pretty clean. And by making it immersive I save 4 spaces of indentation on every line! One step closer to my beloved single-procedure view.


Which brings us to a conversation about conventions and configuration. I love VB’s project-level imports and root namespaces, but I’ve long desired the ability to configure them for a group of files in my project with more granularity, e.g. the ability to assign a root (implicit) namespace to all files in a particular folder (usually representing an architectural layer), and/or to add one or more Imports to all source files in a particular layer/folder. And I can definitely imagine wanting to give a particular immersive configuration to groups of source files based on folder, extension, or just naming convention.

So, ModVB will add the ability to configure these settings and apply them based on a pattern which could describe folders, conventions, and extensions (maybe regex, maybe not):

  • Extensions for type kinds? .vbclass, .vbstructure, .vbinterface, .vbmodule
  • Extensions for base types? .vbxhtml, .vbxaml, .vbsql, .vbform
  • Or conventions for base types *Controller.vb, *Form.vb, *Service.vb

Beyond top-level for learning how to code (which, again, is very important), I’d use immersive files extensively on every project and I’ve been coding in VB.NET for 20 years—I’ve proven I know how to declare a class by now, I’ve earn this!

Key Fields & Properties, and Auto-Constructors

Another thing I’m a little tired of typing after 20 years is very simple constructors for setting fields and properties. I love parameterized constructors! They really were one of the things I was very excited for in VB.NET coming from VB6—the ability to ensure that an object’s state was initialized on creation really appealed to me and it still does. That’s why I continue to initialize my objects through constructors, even for non-public types, instead of just making fields public or other violations of encapsulation. That said, it’s kind of tedious after thousands of times doing it.

To that end, ModVB will allow you to apply a modifier to any class or structure instance fields or properties of your choosing, designating them as Key fields/properties. If no other constructor is provided by the developer, the compiler will generate one with parameters matching the keys and initialize them.

Class AppointmentBookingService
    
    Private Key CalendarService As ICalendarService,
                PaymentService As IPaymentService,
                EmailService As IEmailService

    ' Look at this code I'm never going to have to type again!
    ''Public Sub New(calendarService As ICalendarService,
    ''               paymentService As IPaymentService,
    ''               emailService As IEmailService)
    ''               
    ''    Me.CalendarService = calendarService
    ''    Me.PaymentService = paymentService
    ''    Me.EmailService = emailService
    ''End Sub

End Class

And yes, naming conventions are important to me as they are to many .NET developers, so the compiler will camelCase those parameter names appropriately, though this may be configured. This design has great synergy with the Immersive Files feature because it doesn’t require a class statement. So much boilerplate gone!

Some of you may have noticed that the Key keyword is actually borrowed from VB anonymous types. In vanilla VB, anonymous type properties marked Key contribute to that anonymous type’s implementation of Equals and GetHashCode and are implicitly ReadOnly. Key fields/properties in regular classes or structures retain this capability, but, to ensure the broadest applicability of the feature, they are not required. Rather, if a class or structure implements IEquatable(Of T), a default override of Equals and GetHashCode will be provided by the compiler using these fields/properties, if the developer does not provide explicit overrides of these members.

Structure Money
    Implements IEquatable(Of Money)
    
    Public Key ReadOnly Property Value As Decimal
    
    Public Key ReadOnly Property Currency As Currency
    
    ' Sub New(value, currency), Equals, and GetHashCode provided by compiler.
    
End Structure

Explicitly marking the fields/properties of a type that are … key to its identity and function also allows the IDE to be more guided in suggestions and code generation for other members, such as operators. This leaves open a question of which members should be generated by the compiler by default and which should be suggested by the IDE if the developer begins defining them. For example:

  • A default implementation of the = and <> operators
  • A default implementation of the Deconstruct method for pattern matching

I’m leaning toward the compiler generating the = and <> operators and I’m leaning toward not making an object implicitly deconstructable because that could break encapsulation. One compromise I’m considering though is a simple rule: Unlike the generated constructor, which takes all keys, the Deconstruct method(s) will only expose those keys which are Public, with the rationale that if a Key field/property is already part of a type’s public API, it’s not a violation of encapsulation to also make that value accessible via pattern matching.

Pattern matching isn’t in VB yet so I don’t know how popular these methods are going to be, so ultimately the decision on whether to adopt this convention-based approach will heavily depend on usage patterns and feedback, so sound off in the comments!

Wildcard Lambda Expressions

Idiomatic .NET development has changed massively since v1.0. One of the most impactful shifts came after v3.5 in 2008 with the introduction of lambda expressions. Beyond their necessity to support LINQ, lambda expressions have become fundamental to how all manner of APIs work. One pattern in particular that’s been extremely popular is passing a lambda expression as a kind of strongly-typed “property reference”. Indeed, the origin of this feature was my desire to create a syntactic distinction for values in XAML literals that described data-binding paths:

<TextBox Text={*.PrimaryContact.FirstName}/>

Whether that potential use case ever materializes, the feature is generally useful for the many .NET APIs which take Expression(Of Func(Of T1, T2)) lambdas as a declarative property mapping. I felt the need for this feature especially on a recent-ish project using Entity Framework Core, where even C#’s _ => _.Foo syntax very quickly grew tiresome (so many <SHIFT> key presses, it’s like playing piano)

' Vanilla VB
modelBuilder.Entity(Of Blog) _
            .Property(Function(b) b.Url) _
            .IsRequired()
            
modelBuilder.Entity(Of Blog) _
            .HasMany(Function(e) e.Posts) _
            .WithOne(Function(e) e.Blog) _
            .HasForeignKey(Function(e) e.ContainingBlogId) _
            .IsRequired()
            
' ModVB
modelBuilder.Entity(Of Blog) _
            .Property(*.Url) _
            .IsRequired()
            
modelBuilder.Entity(Of Blog) _
            .HasMany(*.Posts) _
            .WithOne(*.Blog) _
            .HasForeignKey(*.ContainingBlogId) _
            .IsRequired()

I feel the wildcard syntax really captures the spirit of how much I don’t care about the variable.

Of course, these expressions aren’t limited to just property access or just one level so expressions like this will also work

Let users = context.Users.Include(*.Profile.Avatar).ToList()

This raises the question about nesting and what the second `*` below should mean?

Let blogs = context.Blogs.Include(*.Posts.Select(*.Comments))

Maybe the syntax should it be **.Comments, or maybe *.Posts..Comments?

Recently I noticed that F# has already added this kind of feature so maybe I’ll peek at what they do in this case. Sound off in the comments!

SPOILER: Fortunately, in the LINQ section you’ll discover that Include is being added as a new query operator so maybe this was just a bad example.

Markdown Documentation Comment Syntax

While studying the topic of user interaction design (one of my favorite topics), I read somewhere (that I can no longer find) either the statement “That which is easy will be done often (even if it shouldn’t)” or the corollary “That which is hard will not be done often (even if it should)”. The second one is most important to this section. If you want people to “do the right thing” you have to make “the right thing” easy, and certainly not a pain.

I believe in documenting my declarations and I know many other developers who also believe this… in their hearts. But in our fingers, it’s just a little too much to do for non-public APIs. I want to encourage both new programmers and veterans to document their declarations. It’s good for maintainers, it’s good for tooling because documentation is surfaced in IntelliSense. To that end, I intend to allow a dramatically simplified syntax for doc comments. After playing around with a few minimalist designs I’ve decided that rather than demand anyone remember yet another format and despite my general preference for WYSIWYG editing, it would be best to accept a markdown-based format.

'''
''' Returns a function from a description of a line in slope-intercept form.
'''
''' # Parameters
''' - @m: The slope of the line. Must not be @Double.PositiveInfinity,
'''       @Double.NegativeInfinity, or @Double.NaN.
''' - @b: The y-intercept of the line. Must not be @Double.PositiveInfinity,
'''       @Double.NegativeInfinity, or @Double.NaN.
'''
''' # Returns
''' An instance of `Func(Of Double, Double)` delegate type which
''' returns the y-coordinate given an x-coordinate.
'''
''' # Exceptions
''' - @ArgumentOutOfRangeException: Either @m or @b is
'''     @Double.PositiveInfinity, @Double.NegativeInfinity, or @Double.NaN.
'''
''' # Remarks
''' A line can be of one of 3 forms:
''' 1. Horizontal,
''' 2. Vertical, or
''' 3. Diagonal.
'''
''' This API only supports forms 1 and 3.
'''
''' # Examples
''' ## Normal usage
''' ``` vb.net
''' Let y = F(3 / 2, -5)
''' Graph(y, 0 To 100)
''' ```
'''
''' ## Horizontal line
''' ``` vb.net
''' Let y = F(0, -5)
''' Graph(y, 0 To 100)
''' ```
Function F(m As Double, b As Double) As Func(Of Double, Double)
    If Double.IsInfinity(m) OrElse Double.IsNaN(m) Then
        Throw New ArgumentOutOfRangeException(NameOf m)
    ElseIf Double.IsInfinity(b) OrElse Double.IsNaN(b) Then
        Throw New ArgumentOutOfRangeException(NameOf b)
    End If
    
    Return Function(x) (m * x) + b
End Function

The three '‘s are still required (instead of one) to keep devs from accidentally publishing private comments with their public docs.

This is about as minimal as it’s going to get which is great for getting over the laziness threshold to type it but also great for reading online, e.g. viewing PRs on GitHub. No worries, the output format will still be the same XML and XML in the comments will still be supported. In fact, you’ll be able to mix them. There are some great scenarios out there for the open-ended XML syntax such as the <response code=”###”> tag used by ASP.NET for Swagger API documentation generation and I don’t want to break any of them.

#Ignore Warning Directive

For a syntax that supposedly represents ignoring a warning, it sure takes up a lot of space…

' Before.
#Disable Warning BC42356 ' No `Awaits`; don't care.
Async Function SayHiAsync() As Task
#Enable Warning BC42356

    Console.WriteLine("Hello, World!")

End Function
    
' After.
#Ignore Warning BC42356 ' No `Awaits`; don't care.
Async Function SayHiAsync() As Task

    Console.WriteLine("Hello, World!")

End Function

This new directive will ignore the specified warning for the next statement only. Not the next block, the next statement. It’s actually pretty rare that I interact with warnings in VB or would choose to ignore them, but with the rise of code analyzers this is sure to become more common, so I’d like to be able to ignore them in a way that I can mostly… ignore.

New Implicit Line Continuations

VB’s implicit line continuation has been an incredibly impactful addition to the language and an elegant solution to allowing more flexible formatting of code without requiring any explicit line termination. I haven’t heard many requests for new places to allow them over the years but here’s what I have heard so far

  • Before Then
If someComplexConditionExpression1 AndAlso
   (someComplexConditionExpression2 OrElse
    someComplexConditionExpression3)
Then
    ' Do stuff.

End If
  • Before Handles
Sub Button1_Click(sender As Object, e As MouseMoveEventArgs)
    Handles Button1.MouseMove

    ' Do stuff.
End If
  • Before Implements
Function GetEnumerator() As IEnumerator(Of T)
    Implements IEnumerable(Of T).GetEnumerator
    
    ' Get thing.
End Function
  • Between ) and As in Function statements
Function BuyCar(
           make As String,
           model As String,
           year As Integer,
           Optional trim As String
         )
         As Car

Of course, any changes in implicit line continuation have to be very carefully considered with regard to the IDE experience—the IDE has to be pretty smart about whether the user is done typing or not and I can’t regress any muscle memory in common scenarios. With that said, also I’m considering

  • Maybe allowing comments between query clauses?
' Getting customers
From customer In db.Customers
' in Illinois
Where customer.Address.State = "IL"
' with orders
Join order In db.Orders On order.CustomerId = customer.Id
' made last year
Where order.OrderDate.Year = 2023
' from most expensive to least
Order By order.Total Descending
' getting the top 10
Take 10

If this would help you, please send me a better example (instead of my contrived one where the comments are completely redundant)!

  • VERY UNLIKELY: Before commas (,) and various binary operators like AndAlso and OrElse?

I know it’s pretty common in SQL and other languages to put the conjunction on the next line so you can delete or comment out trailing expressions in a list easily but that could be pretty invasive to the typing experience in many scenarios. It would take a lot of analysis to understand the impact of this request, which I’ll happily do at some point in the future. As it stands this is very unlikely.


Please let me know if you have more scenarios. I know I want to do something about leading .‘s and calling fluent-APIs but right now I’m leaning more toward an IDE-focused solution for a few reasons, so I’ll discuss that design in a future IDE-focused post.

Bug Fixes & Other Minutiae

Now the lightning-round!

  • Async Main methods will be legal
Async Function Main(args As String()) As Task(Of Integer)

    Await Console.Out.WriteLineAsync("This is an example.")

    Return 0
End Sub

There’s no principled reason it wasn’t before, just never got around to it.

  • Optional parameter default value inference

Reusing an earlier example

Function BuyCar(
           make As String,
           model As String,
           year As Integer,
           Optional trim As String ' <-- No default here.
         )
         As Car

Typing = Null and = Nothing over and over again doesn’t actually help me. It’s just noise and it obscures the important cases where the default value isn’t just the parameter type’s default value.

  • Optional keyword … optional after first optional parameter?
Function MakeDateTime(
           year As Integer,
           month As Integer,
           day As Integer,
           Optional
             hour As Integer,
             minute As Integer,
             second As Integer,
             millisecond As Integer,
             microsecond As Integer
         )
         As Date

Because in VB optional parameters must be at the end of a parameter list, the keyword needn’t be repeated for every subsequent parameter after the first.

  • Type inference for accessor value parameters
' Before
Property Name As String
    Get

    End Get
    Set(value As String)

    End Set
End Property

' We shall never know the delegate type of this event.
Custom Event NameChanged As EventHandler(Of EventArgs)
    AddHandler(value As EventHandler(Of EventArgs))

    End AddHandler
    RemoveHandler(value As EventHandler(Of EventArgs))

    End RemoveHandler
    RaiseEvent(sender As Object, e As EventArgs)

    End RaiseEvent
End Event

' After
Property Name As String
    Get

    End Get
    Set(value)

    End Set
End Property

Event NameChanged As EventHandler(Of EventArgs)
    AddHandler(value)

    End AddHandler
    RemoveHandler(value)

    End RemoveHandler
    RaiseEvent(sender, e)

    End RaiseEvent
End Event

I don’t know that these type specifiers help folks, but they do make changing a property or event type more error prone.

A little-known secret, the value parameter list in Set accessors has always been optional. If you delete it (including the parentheses) the compiler declares an implicit parameter named Value but the IDE always generates it for clarity. AddHandler and RemoveHandler should also do this to be consistent though personally I prefer having an explicit parameter.

  • Custom keyword in for custom Event declarations not required?

Also did you notice that the second event declaration in the previous example didn’t have a Custom modifier?

When Custom events were introduced to VB I suspect that the Custom keyword made parsing expanded event declarations easier, but when auto-implemented properties were introduced it became apparent that this keyword wasn’t necessary to distinguish the one-line declaration from the multi-line one. While I do find the modifier useful for establishing the vocabulary of how we VB devs refer to such events, I feel compelled to ensure properties and events are consistently flexible here.

  • Optional parentheses for NameOf expressions
Public Shared ReadOnly HeaderTextProperty As DependencyProperty =
                         DependencyProperty.Register(
                           NameOf HeaderText, ' <-- No parentheses.
                           GetType(String),
                           GetType(MyControl)
                         )
                                            
Public Property HeaderText As String
    Get
        ...

Because of the way VB NameOf is designed (it’s a reserved keyword) I don’t think these parentheses were ever required. They’re an artifact of thinking of it like GetType() when I should have been thinking of it like AddressOf. What do you think—parens or no parens?

  • Open generic types/methods(?) allowed in NameOf expressions
' Vanilla VB
' This is legal:
Shared ReadOnly DictionaryType As Type = GetType(Dictionary(Of,))

' But you're required to write this:
Shared ReadOnly DictionaryName As String = NameOf(Dictionary(Of Object, Object))

' ModVB
' Now this is also legal:
Shared ReadOnly DictionaryName As String = NameOf Dictionary(Of,)

VB allows “open generic types” in GetType operands but not NameOf operands; in NameOf we require developers to supply meaningless dummy type arguments. I’m pretty sure this inconsistency isn’t necessary in VB and is just a historical artifact.

Wrap up

Remember, Folks: The more you say it, the less special it becomes…

I love VB’s declarative and descriptive syntax and will always prefer it over more terse languages with heavy uses of symbols with special meanings. That said, I’ve been doing this VB.NET thing for a couple decades now and there are a few common patterns of coding that I’d like to clean up just a little bit so I can focus more on what matters. Hopefully, you agree with these changes and if not or if I’ve overlooked something that matters to you, don’t hesitate to sound off in the comments!

Thanks for reading! Please share and discuss everywhere!

Type-Inference Enhancements | Overview ↑ | Next Section Coming Soon! →

13 thoughts on “Streamlining and Boilerplate Reduction

  1. The pattern below is extremely common in Roslyn in C# and VB#If something

    #Else

    #EndifPublic Sub Something()

    but in VB the declaration must be repeated.

    if you search #if in the C# code you will find many useful cases that can’t be replicated in VB without duplicating code in both paths making the VB code hard to understand and error prone to exit.

    with _ ‘ you can almost put comments anywhere C# can but formatting gets messy. Your additions probably remove the “almost”.

    Liked by 1 person

    • I think what you type is getting stripped out. I’m guessing it was attributes though. Very good case to bring up. Let me noodle on it.

      Like

      • Yes you can’t break a declaration statement within an #If the most common case is Attributes but also Public, Friend, Private can’t be conditional.

        Like

    • I once worked on a C# project where a EF repository was shared between a .NET Framework and a .NET Core project and specific clauses in the middle of query were `#if` conditional between them because the way you’d do it in EF or EF Core was different. I remember thinking about how in VB I’d need to duplicate the whole query (in the easy case that it’s just a 2-way check and not some 2x2x2 thing).

      And on a VB project back in… maybe 2004, I tried to share code between .NET Framework and .NET Compact Framework, which didn’t have the `<Serializable>` attribute and my classes were serialized. Same problem. Thanks for the reminder!

      Liked by 1 person

  2. Maybe one for the future – one that I encounter a lot:

    ‘Vanilla VB:Class x

    Overridable Function y as xReturn new xEnd Function

    End Class

    Class qinherits x

    Overrides Function y as x ‘must be x because it was defined as x in ancesterReturn new q ‘means I have to cast it when I use it ….End Function

    End Class

    ‘ModVB:Class x

    Overridable Function y as MyType ‘Returns x, because that is the current typeReturn new xEnd Function

    End Class

    Class qinherits x

    Overrides Function y as MyType ‘Returns q, because that is the current typeReturn new q ‘Always the correct typeEnd Function

    End Class

    Liked by 1 person

    • Tell me more about why you encounter it a lot. I’ve run into it a few times but I don’t trust myself. ICloneable is an example. I do intend to propose relaxed overrides as you describe in the section on inheritance regardless but I would like more concrete use cases!

      Like

      • I encounter this in business software; IClonable is a good example, but also on factory methods (create new instance of me), input methods (file to instance), conversion (convert instance of class x to an instance of me).

        Like

  3. I have a question about the type inference of RaiseEvent. Is it possible to omit types in the parameter list?

    Before

    raiseevent(sender as object, e as EventArgs)

    After

    raiseevent(sender, e)

    Liked by 1 person

  4. Why not implicitly declare a value parameter with the same type as the property type in the setter and make the declaration optional?

    Property Name As String

    Get

    End Get

    Set

    m_name = value

    End Set

    End Property

    Like

    • VB already works this way today. Delete the parameter list to see for yourself. It’s always been optional but the IDE puts it in. This is more a middle ground.

      Like

Leave a reply to ojburg Cancel reply