dotnet/vblang

Concessional Extensions `Let`, `Next`, and `Incr`

rskar-git opened this issue ยท 16 comments

It seems to be a pain point for many not to have these C# (and Java and C++ and Javascript...) operators of = (inline assignment), ++i (pre-increment) and i++ (post-increment) operators. Obviously (to those who know modern VB), these can be emulated well enough, especially via extentions. E.g.:

<Extension>
Function [Let](Of T)(ByRef x As T, result As T) As T
    ' Inline assignment
    x = result
    Return result
End Function

<Extension>
Function [Next](ByRef x As Integer) As Integer
    ' i.Next works like i++
    Dim x0 As Integer = x : x += 1 : Return x0
End Function

<Extension>
Function Incr(ByRef x As Integer) As Integer
    ' i.Incr works like ++i
    x += 1 : Return x
End Function

I'm proposing coming up with something to standardize the VB equivalents to these, so that translations between C# and VB can go more smoothly. Among the more common complaints against VB is the lack of examples versus what's available in C#, and the use of code translators as a means to cope often get tripped up on = and ++. See for example https://stackoverflow.com/questions/45552216/bitcoin-blockchain-parser-c-sharp-snippet-to-vb.

I'm thinking that if (built-in) standard VB equivalents to = and ++ are made, then people who make code translators will make use of them. That in turn will facilitate VB code examples for leading-edge developments.

Regarding inline assignment: #190 (comment)

@reduckted Thanks for the comments link. I understand the philosophic stance which @AnthonyDGreen has detailed there. But I want to make it perfectly clear that I am not advocating for (new to VB) operators, and I'm not pushing for there to be more inline assignments etc. in VB coding practices.

What I'm specifically suggesting is that there be a set of "official" "canonical" extensions that can take the place of = and ++ specifically for the purposes of easing translations of C# code into VB code.

Interesting... Yes, translators... (strokes his facial hair contemplatively)

@rskar-git What I'm specifically suggesting is that there be a set of "official" "canonical" extensions that can take the place of = and ++ specifically for the purposes of easing translations of C# code into VB code.

This is definitely interesting, I can see the use case (i.e translators). However, to make these "official" translations, they would have to be used by an official translator...I don't know if there's such a thing (the Refactor tool comes close and is thoroughly impressive) but maybe there should be (I can imagine it would make all the documentation writer's lives much easier: write a code sample in language X and voila: it can be translated to other languages Y and Z automatically or at least get you 95% of the way there so you can do the rest by hand) ๐Ÿ‘

@ericmutta >...they would have to be used by an official translator.

Nope, they would simply have to be reliably present in current (and future) editions of VB. At that point, people who work on translators would naturally begin to make use of them.

E.g., check out the stackoverflow link I put above. It made this bit of C#:

var header = new byte[8];
int index = 0;
var magic_signature = ((uint)header[index++] << 0) | ((uint)header[index++] << 8) | 
    ((uint)header[index++] << 16) | ((uint)header[index++] << 24);

into this for the magic_signature:

Dim magic_signature = (CUInt(header(Math.Max(Threading.Interlocked.Increment(index), index - 1))) << 0) Or
(CUInt(header(Math.Max(Threading.Interlocked.Increment(index), index - 1))) << 8) Or
(CUInt(header(Math.Max(Threading.Interlocked.Increment(index), index - 1))) << 16) Or
(CUInt(header(Math.Max(Threading.Interlocked.Increment(index), index - 1))) << 24)

Notice what that translator did. It used CUInt for the (uint) type-cast - which is good - instead of CType(x, UInteger). CUInt is officially a part of VB, and they used it.

Now look at what it did for index++: Math.Max(Threading.Interlocked.Increment(index), index - 1). They went that way because it's using things that are officially part of VB, in as much as the .NET Framework is an essential part of VB, and reliably present. It's also an ugly mess.

Some translators might actually introduce Private Shared Functions for something like index++ as part of their process, but the outcomes also tend to be ugly (see https://github.com/icsharpcode/CodeConverter/blob/master/Tests/VB/SpecialConversionTests.cs, look for Function __InlineAssignHelper(Of T)).

Now if something like <Extension> Function [Next](ByRef x As UInteger) As UInteger was already a VB thing, the translator developers could have easily made it come out like:

Dim magic_signature = (CUInt(header(index.Next())) << 0) Or (CUInt(header(index.Next())) << 8) Or
(CUInt(header(index.Next())) << 16) Or (CUInt(header(index.Next())) << 24)

Therefore no need for any .NET Framework shenanigans or makeshift helper functions.

OTOH, is this an "official" converter?: https://roslyncodeconverter.azurewebsites.net/

@rskar-git What I'm specifically suggesting is that there be a set of "official" "canonical" extensions

Ah, I'm with you now. These could be added to the Microsoft.VisualBasic assembly.

My thoughts on naming:

Function AssignValue(...)
Function PrefixIncrement(...)
Function PostfixIncrement(...)

Using "prefix increment" and "postfix increment" as the names will make it easier for people to understand what it's actually doing, because that's what the C# docs use: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/increment-operator

Still not sure if that makes it clear what they do. Maybe this ๐Ÿคฃ:

Function AssignValueAndReturnAssignedValue(...)
Function IncrementValueAndReturnOriginalValue(...)
Function IncrementValueAndReturnIncrementedValue(...)

One downside I can see with these extensions, is that there is no way to handle overflows. Because the operation happens in another assembly, you can't control whether overflow checks occur, which means you'd want two versions - one with overflow checks and one without.

As an aside, while writing this and testing some code I had to triple-check the docs to make sure I had the "operators" behaving correctly - reminds me of this article from Eric Lippert (see point number 3): http://www.informit.com/articles/article.aspx?p=2425867

When writing this article, I had to open the specification and double-check to make sure that I wasn't remembering it backward, and this is after using these operators for 25 years and writing their code generators in several compilers for several languages. I surely cannot be the only person who finds mnemonics for these operators to be utterly useless.

Can there be decrement operators too?

Absolutely. No point having one without the other. ๐Ÿ‘

@rskar-git Nope, they would simply have to be reliably present in current (and future) editions of VB.

I hear you!

Given the move to a more modular .NET Framework that ships as a collection of NuGet packages, perhaps the first and fastest move would be create a project here on github for two assemblies: "Translate.CsToVb.dll" and "Translate.VbToCs.dll". These would contain extensions such as your proposed Let and ship as NuGet packages that can be referenced by any translator. Then it would be a matter of championing them to those working on translators. If they gain momentum and become a de-facto standard, I am sure Microsoft would pick them up (if only to help them address the massive "write samples in both languages" problem).

Give it a go, we'll rally behind you and help where we can ๐Ÿ‘

PS: there's a very nascent discussion at #238 on mixing C# and VB in one project. If that pans out, it may eliminate/reduce the need for translators (you would literally just copy the code verbatim and stick it in a foreign language source file that would be compiled and referenced automatically in the host project).

@ericmutta perhaps the first and fastest move would be create a project here on github for two assemblies

Another solution would be to make a PR for icsharpcode/CodeConverter (which appears to be the de facto converter) that causes it to add local methods for assign, increment and decrement, instead of using Interlocked.Increment, etc.

(Actually, just having a look at the code, it already appears to do this for inline assignments - https://github.com/icsharpcode/CodeConverter/blob/a81137d15ade75a2061d4b480222e9710970b873/ICSharpCode.CodeConverter/VB/NodesVisitor.cs#L51-L55)

These would contain extensions such as your proposed Let and ship as NuGet packages that can be referenced by any translator.

I'm not really sure that creating a NuGet package with the extension methods would actually solve anything. The code converters are not the things that need to use the extension methods - it's the converted code that needs the extensions.

@reduckted > Function AssignValueAndReturnAssignedValue(...) ... etc.

Someplace somewhere you just made some Cobol coder blush with joy! ๐Ÿ˜„

As far as namings go: Whatever the respective official "committee(s) of taste" decides on, I'm sure I can cope. I of course would prefer something terse, even if a tad mnemonic, but OK. (Frankly I don't see a problem in going with Let for inline assignment. In ye olde BASIC, that was how assignment was done; in modern VB it lives on as a keyword for Linq. F# and JavaScript use it.)

As far as handling overflows is concerned - Integral types: I would say only bother with the unchecked integer versions of extensions for ++ and --. At least 99% of the time this is all that's needed. It is, afterall, in the idiom of C/C++/Java/C# to be doing unchecked integer arithmetic by default anyway. The worse case scenario is where the C# coder intended overflow exceptions to be possible, which I'd bet is going to be a rare enough thing to not be worth the worry; a good VB coder is going to be doing some proper refactoring after a translation, anyway.

As far as handling overflows is concerned - Other numeric types: We need not worry. Overflows/underflows can happen, even in C#. Not aware of any unchecked float arithmetic in C#.

@ericmutta Mixing C# and VB in one project (as seems to have emerged from #238, which is really about full VB Support to Xamarin) sounds magical, yet I wonder on how that can be made a practical reality. In particular, what sort of refactorings and enhancements will be needed in the IDE to allow for a streamlined experience in typing and debugging?

.NET already supports the notion of a .netmodule (https://blogs.msdn.microsoft.com/junfeng/2005/02/12/netmodule-vs-assembly/). That is the obvious thing to leverage. A VB project can already reference another project (of any language) that is also a member of its solution. Maybe one could devise a way to indicate that a project should be compiled as a .netmodule and linked-in instead of as a DLL to be referenced?

Here's how I'd imaging such a scheme might work:

Let Project V represent our (primary) VB project which results in either an EXE or DLL assembly. Let Project C represent (for example) a C# project, which must result in a DLL assembly. Have projects V and C be members of the same solution (the *.sln file).

In project V, have it reference project C - not its DLL! But its project (*.csproj)!

Now somewhere, somehow, within the properties of project V, have some switch set to enable project C to be a netmodule. Perhaps tucked away on the Advanced Compiler Settings, there could be a list of eligible projects with checkboxes, and checked means make it a netmodule. (Perhaps to make a project "eligible", it must be a DLL project and also not make use of this netmodule scheme itself.)

The "Debug" configuration compiles project C into a DLL, and project V references the DLL. (This should ease the burden on keeping the IDE capable of delivering a streamlined typing and debugging experience.) Members marked as internal (or Friend) may be somewhat problematic, but perhaps can be addressed by way of the InternalsVisibleToAttribute (https://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.internalsvisibletoattribute(v=vs.110).aspx, https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/assemblies-gac/friend-assemblies).

The "Release" configuration compiles project C into a netmodule and links it into the resulting project V assembly. (This will likely bring compromises to the IDE debugging and typing - but that's already the case anyway without netmodules.)

So there you have it. Use the "Debug" configuration for development/debugging/testing, and use InternalsVisibleToAttribute as needed; the IDE should work fine since things are as they are now for multi-project solutions. Then switch to the "Release" configuration and do the compile, and all the satellite "Friend Assemblies" are become linked-in netmodules into your primary VB project assembly.

Of course, I have no idea on the depths of my naivety here, let alone if it's of any help to the Xamarin problem.

Been thinking about this more.

Can this be used for non-built-in types? If so, if I overload both ++/op_Increment() and +/op_Addition() in my class, then call myObj.Next(), which gets invoked?

I don't know what kind of voodoo magic this Threading.Interlocked.Increment/Decrement thing is... but it's AWESOME!!!

I'm playing with this idea and my thoughts are basically three extension methods to handle:

int value = 1;
Console.WriteLine($"({value}) value=5 = {value=5} ({value})");
Console.WriteLine($"({value}) value++ = {value++} ({value})");
Console.WriteLine($"({value}) value-- = {value--} ({value})");
Console.WriteLine($"({value}) ++value = {++value} ({value})");
Console.WriteLine($"({value}) --value = {--value} ({value})");

in VB...

Dim value As Integer = 1
Console.WriteLine($"({value}) value=5 = {value.Assign(5)} ({value})")
Console.WriteLine($"({value}) value++ = {value.Incr} ({value})")
Console.WriteLine($"({value}) value-- = {value.Decr} ({value})")
Console.WriteLine($"({value}) ++value = {value.Incr(Apply.Before)} ({value})")
Console.WriteLine($"({value}) --value = {value.Decr(Apply.Before)} ({value})")

I've placed these in an "Inline" module that contains:

Enum Apply with Before and After

Assign(value)
Incr([apply])
Decr([apply])

Assign is generic typed, while the Incr and Decr are overloaded to handle different types. I'm thinking that there doesn't really need to be a prefix / postfix version of the inline increment and decrement. In both cases you are incrementing - the only difference between the two is whether or not you will apply the increment before or after usage within the expression; thus my approach.

However, the voodoo magic that is Interlocked.Increment only allows for this on Integer, UInteger, Long and ULong types. I don't believe (or at least don't understand) how this could possibly work for Byte, SByte, Char, Short, UShort, Single, Double, etc.

I do a lot of C# to VB conversions (both using tools and by hand); so this idea intrigues me greatly. I'm also considering adding these into (at some point) https://github.com/DualBrain/Community.VisualBasic

Thoughts?

Help on the function
https://docs.microsoft.com/en-us/dotnet/api/system.threading.interlocked?view=net-5.0

It is part of https://github.com/dotnet/coreclr and is open source. There is a PR to make it handle more types.