Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Try assignment/initialization #190

Closed
AnthonyDGreen opened this issue Oct 20, 2017 · 6 comments
Closed

Try assignment/initialization #190

AnthonyDGreen opened this issue Oct 20, 2017 · 6 comments
Assignees

Comments

@AnthonyDGreen
Copy link
Contributor

Related: #175, #159 #132 #67 #60

A general solution that solves both multiple return values and the popular .NET TryXYZ pattern eludes me. Though it would seem that the "Try" pattern would benefit from using either nullable value types or tuples, neither approach is satisfactory.

Nullables

Dim i As Integer? = Integer.TryParseNullable(s)

If i IsNot Nothing Then
    Dim i2 = i.Value ' To avoid the on-going tax of unwrapping the null.
End If

myDictionary(myKey) = Nothing

Dim myValue = myDictionary.TryGetValueNullable(s)
' Null was a valid value here.

Tuples

Dim r = Integer.TryParseTuple(s)

If r.Succeeded Then
    Dim i = r.Value ' To avoid the on-going tax of unwrapping the tuple.
End If

Dim (succeeded, result) = myDictionary.TryGetValueTuple(s)

If succeeded Then
    ' Do whatever.
End If

' succeeded just hangs around forever despite its transient role having long ago ended. Lamentations. 

It turns out that ByRef and Out remain the preferred solution to this problem. This leads customers to ask us for better support for Out vars like C# has. However, VB has long shied away in general from inline assignment and inline declarations and I am loathe to break from this. I find keeping declarations and assignments at the statement level to simplify code and indeed the use of inline assignment trickery in particular is frowned upon even in curly brace languages.

After some thought, I realize that the reason general data-based solutions don't address this problem is because the "Try" pattern simultaneously conveys both data-flow and control-flow. It's meant to be used in conjunction with an If or other control-flow statement and the assignment of a value is relegated to a side-effect of the control-flow. I propose that a special solution is needed for this pattern that recognizes the duality: Try assignment/initialization.

Single-line

Dim offset = Try Integer.TryParse(offsetString) Else Throw New FormatException
Dim count = Try Integer.TryParse(countString) Else Throw New FormatException

Fallback

Function Factorial(n As ULong) As ULong
    Static cache = New Dictionary(Of ULong, ULong)

    Dim result As ULong

    If n = 0 Then
        result = 1
    Else
        result = Try cache.TryGetValue(n) Else
                     result = n * Factorial(n - 1)
                     cache(n) = result
                 End Try
    End If

    Return result
End Function

Positive consequence/early-out

Dim root = Try document.TryGetRoot(cancellationToken) Then
               Return root ' Early return.
           End Try

' Do something heavy.

The above examples reverse the inversion the "Try" pattern forces on developers by making the control-flow aspect subordinate to the data-flow aspect. This allows type-inference to work as normal and keep variable declarations and assignments at the statement level. I considered other designs made this a new kind of statement but all of them lost those benefits.

The specifics of what patterns the feature would work on (nullables, tuples, out-vars) haven't been specified. But if designed correctly this could yield a very natural transformation for an asynchronous try:

Dim customer = Try Await repository.TryGetCustomer(id) Else Return "Not Found"

Return customer.Name 

Such a feature, combined with tuples completely eliminates the need for ByRef and Out arguments outside of performance-based structure copy-avoidance; a very uncommon scenario.

Feedback from 2017.10.18 LDM

  • Are side-effects really that bad?
  • Syntax is weird because the right-hand side of the assignment can assign to the thing it's initializing; should it instead be forced to return a different value?
    • But how would we represent the case where a failed "Try" should result in throwing or early exit?
@reduckted
Copy link
Contributor

VB has long shied away in general from inline assignment and inline declarations and I am loathe to break from this.

Why have VB shied away from this? And can you explain what you mean by "inline assignment" and "inline declarations" (for clarity)?

...and indeed the use of inline assignment trickery in particular is frowned upon even in curly brace languages.

Then why did C# add out variables?

Syntax is weird

100% agree. It's difficult to read. In one case, the block inside the Try is the "true: case, and in another it's the "false" case, and the only thing that differentiates the two is the keyword on the end of the line. Let's play spot the difference:

Dim result = Try cache.TryGetValue(n) Then
                 ' Is this true or false?
             End Try

Dim result = Try cache.TryGetValue(n) Else
                 ' Is this true or false?
             End Try

Not that easy to see the difference.

I fail to see how this solution is any better than #60 (comment)

If cache.TryGetValue(n, Dim result) Then
    ' Do something "result".
End If

@AdamSpeight2008
Copy link
Contributor

AdamSpeight2008 commented Oct 21, 2017

I think Proposal #191 is a better generalisation.

@AnthonyDGreen
Copy link
Contributor Author

@reduckted,

Not entirely sure why. It's just been a thing that's been conspicuously absent for 53 years. And we shouldn't hastily discard long-standing design decisions because we don't understand them. Some languages like inline assignment expressions, some languages don't. BASIC is in the don't category, along with F#. I can advance some theories:

  1. BASIC was originally written to make computing accessible to mathematicians. That's why it has an exponentiation operator ^. Inline assignment has no analog in mathematics.

  2. BASIC uses the same token = for for both assignment and comparison. It's the context that determines which it means. VB could have at any time adopted different operators for those two things except that's a terrible design and languages which use it are morally bankrupt.

  3. Assignment expressions are the source of an entire class of bugs that have plagued languages which have it for generations. In C++, this pattern of code:

if (NULL == variable) ...

exists purely to avoid this common mistake. By putting the constant on the left you can avoid accidentally typing this:

If (variable = NULL) // Assigns null and evaluates as false.

And despite the fact that in C# one could have always written:

String s;
if ((s = obj as string) == null)
{

}

Most developers I talk to refuse to write their code this way because such patterns are generally frowned upon.

  1. Side-effects in the middle of expressions add some complexity to reading code. It might not be much for the experienced coder but it is more complexity. Deeply embedded in an expressions variables may be coming into existence, may be changing values, etc. Today you can find the local variables in scope by looking purely at the left side of the statement level.

My point is that it's not a historical accident or a technical constraint that has kept us from doing this, it's a repeated design decision. It's also worth noting that when we first discussed out var in C#, we also discussed generalizing them to a declaration expression that could appear anywhere in C#. We also discussed them for VB and rejected the idea. And when we discussed even the paired-down Out variable idea with MVPs we got strong feedback that the scenario wasn't important, or at least wasn't nearly as important as many other things we'd talked about that added broad value.

Having said all of that, the VB LDM also rejected this idea. After discussing further, I think that what we're working on with nullable types could actually solve this problem with a little work.

' Function Integer.TryParse(s As String) As Integer?

Dim value = Integer.TryParse("1")
If value IsNot Nothing Then
    ' value is not nullable in this scope.
End If

If value Is Nothing Then Return
' value isnot nullable after this point.

With the nullable reference type feature this would be a fine pattern. We can then consider whether to do an F#-style transformation of bool/out/try methods to nullable returning versions.

@paul1956
Copy link

paul1956 commented Nov 2, 2017

Not sure this is the right place to post this but please, as a long time VB developer, I would love if you just remove the requirement to initialize the " variable before calling the function. The syntax is already there, but I still have to initialize dpiX and dpiY before I call the function. VB allows but as far as I can tell does nothing with it. This would be a non breaking change and would make the code clearer since there is no guarantee that the variable would not be changed even if the function returns false, my initialization is meaningless (and confusing). I agree all this other stuff could be done in the future or not at all (my preference given other priorities).

    <DllImport("Shcore.dll")>
    Private Function GetDpiForMonitor(<[In]> hmonitor As IntPtr, <[In]> dpiType As Monitor_DPI_Type, <Out> ByRef dpiX As UInteger, <Out> ByRef dpiY As UInteger) As IntPtr
    End Function

@AnthonyDGreen
Copy link
Contributor Author

@paul1956 we have decided to address that warning scenario you mentioned specifically.

@paul1956
Copy link

paul1956 commented Nov 2, 2017

@AnthonyDGreen Thanks I did not see that anywhere.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants