-
Notifications
You must be signed in to change notification settings - Fork 64
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
Option Infer Turbo (aka Smart Variable Typing) #172
Comments
@AnthonyDGreen If TypeOf obj Is T0 OrElse TypeOf obj Is T1 OrElse TypeOf obj Is T2 ... Then
' What's the type of obj ?
End If |
No , because the only type obj could have would have to be a union type, which we don't have in VB/C# yet. And even then to interact with it would require separate type checks. I guess given two class types we could compute the most derived common ancestor. That could be pretty neat, actually. |
I thought more on it, I think nearest common ancestor would be very complicated implementation wise and in that case it's enough to explicit type obj as the nearest common ancestor to get the same effect. I would like nearest common ancestor for ternary If though... |
Is this a real type change of the local variable (which implies either a change to the runtime, IL or lots of injected type conversions each time its referenced) or a new variable using the same name? If its a new variable, what happens if you assign to the variable inside the block and then reference it outside the block? Dim obj As Object = "Initial Text"
If TypeOf obj Is String Then
obj = "Different Text"
End If
Console.WriteLine(obj) What text gets written? |
See the section "Are the variables immutable?". I think it answers your questions. |
Thank you for an awesome long weekend holiday read. Really fancy that language inferences from works in Typescript (and typeless JavaScript) has an influence in how we can resurrect an old keyword like Q: Does intellisense not get confused when you hover over different variables and it decides that there's a more specific type in play within the block? |
IntelliSense doesn't get confused so long as the compiler doesn't. It just asks the compiler for its understanding of the identifier under the cursor and displays the result. Sometimes there's some extra smarts to link up related but otherwise separate entities. |
@AnthonyDGreen Greatest Common Type (GCT) is already used in in array literals, so it should be possible to use it in the multiple possible type scenario. |
@AnthonyDGreen Or am I thinking of Dominant Type. |
That is dominant type. |
Kind of reminds me pattern matching, although it's not really the same thing. Is this supposed to be "VB version" of pattern matching or would VB eventually get both features? |
Pattern matching is a general term that can mean different things. What I've referred to in the past as "Type Case": Select Case obj
Case t As T
End Select could be described as a special-case of pattern matching or it could not. For Visual Basic 2015 we were originally looking at doing it stand-alone (as a simple extension to That said, there are still scenarios beyond type-checking for which pattern matching could add value. Proposals #101, #140, #139, #141, #160, and #124 discuss those scenarios. So it's not so much that this is pattern matching or an alternative to all pattern matching. It is one approach to addressing a common programming scenario which could also be solved by some forms of pattern matching. Some languages take this approach and others rely solely on pattern matching, however they are not mutually exclusive. That said, whatever pattern matching VB does get will depend on the merits of those other scenarios and right now #140, and to a lesser extent #139 and #101 are the only scenarios that I feel would significantly move the needle for VB users (myself included). The rest seems neat but uncommon. What do you think? |
|
I think the perfect syntax can result from combining the Anthony's proposal with mine, so, we need to declare no new variables to deal with the target type. The Select TypeOf O
Case Nothing
Console.WriteLine("Nothing")
Case String
Console.WriteLine(O[0])
Case Date
Console.WriteLine(O.ToShortDateString( ))
End Select Which can be lowered to: If O is Nothing Then
Console.WriteLine("Nothing")
ElseIf TypeOf O is String Then
Dim O1 = CType(O, String)
Console.WriteLine(O1[0])
ElseIf TypeOf O is Date Then
Dim O2 = CType(O, String)
Console.WriteLine(O2.ToShortDateString())
End Select which can avoid any complications in Anthony's proposal. |
Please clarify what complications you are referring to. Your syntax is irrelevant to this proposal, which discussing aliasing the current variable to the type described by the This: If TypeOf O Is Nothing Then
Console.WriteLine("Nothing")
ElseIf TypeOf O Is String Then
Console.WriteLine(O(0))
ElseIf TypeOf O Is Date Then
Console.WriteLine(O.ToShortDateString( ))
End If could also be lowered in the way you describe, without introducing any new syntax. And if what's bothering you is the clunkiness of the |
When me mental health allows I am still investigating a Select Case obj
Case Is Nothing
Console.WriteLine("Nothing")
Case Is String Into S
Console.WriteLine($"Is String {S}"
Case Is Date Into D
Console.WriteLine($"Is Date {D}")
Case Else
End Select It would be a great fit with Case Is String Into S When S.Length > 2
Console.WriteLine($"Is String {S}"as well |
I often get asked why when you perform a type-test the variable doesn't magically get that type inside the
If
.There's a list of reasons why it's not that simple but I think I have a design that addresses them.
Back-compat
This is worth burning an
Option
statement on. When we added local type inference it would have been a breaking change so we addedOption Infer
for back-compat reasons, leaving itOff
on project upgrade butOn
for new projects.Caveats
ByVal
) parameters.This avoids problems where a property would return a different object of a different type on subsequent invocations, or a field or
ByRef
parameter is mutated on a different thread or even the same thread. Given that both the current pattern of firstTryCast
ing the value into a local and then testing it for null, as well as pattern matching also require this it's not a detractor vs alternatives.How does it work under the hood?
I think of it like a leaky binder. When you have constructs which have boolean conditionals there's an opportunity for a binder (context) to "leak" out either on the "true path" or the "false path" depending on the operators involved. In this context there's a sort of shadow variable with the same name as the variable being tested with the type of the type test.
So, for example take the expression
TypeOf obj Is String AndAlso obj.Length > 0 OrElse obj Is Nothing
the binder "leaks" into the right operand of theAndAlso
operator so in that context 'obj' refers to the String typed 'obj' variable, not the original one. It doesn't leak into the right side of theOrElse
because that's not on the "true path". By contrast in the expressionTypeOf obj IsNot String OrElse obj.Length = 0
the binder does leak into the right hand of theOrElse
becauseTypeOf ... IsNot ...
leaks on the "false path".This is what lets guard statements work:
The "scope" of the binder is everything after the
If
statement (within the same block). This means that within that scope overload resolution will always treat obj as aString
.This leaking has to apply to the short-circuiting logic operators, the ternary conditional operator,
If
,Do
, andWhile
statements and maybeWhen
clauses on exceptions. So, for example:This all happens during "initial binding"; it's not based on flow-analysis.
What about
Where
clauses in queries?We can go one of two ways.
You only get the strong typing within the where if the expression is joined with a boolean operator or conditional because we can't know if the
Where
clause actually executed the lambda and the use of this feature should never result in exceptions.We could translate the
Where
into aLet
, aWhere
, and then aSelect
. It's a big of a stretch but we're already doing magic on this feature so...Does it automagically upcast?
This doesn't happen if the type test would widen the type of the variable so:
What if the same variable is tested multiple times?
The types are intersected. We actually support intersection types in generic methods today when a type parameter has multiple constraints. It's the one place in the language where you can say something is an
IDisposable
AND anIComparable
so we should follow all the same rules there.What about value types?
The idea is that this feature creates strongly typed aliases to objects. So the scenario for testing for a value type necessarily requires a boxed value type on the heap. Today when you unbox a value type from the heap we immediately copy it into a local variable so that any mutation to the copy doesn't change the value on the heap. For this feature we want to preserve the idea that it's just a strongly-typed reference, not a copy, and IL lets us do this. The
unbox
IL instruction actually pushes a managed reference on the stack. Instead of copying the value type we can copy this reference into a "ref local` (and this would be transparent to the user) so a mutation to that value either through say an interface method or the typed value will be consistent. It's critical to preserve identity.Are the variables immutable?
No. But here are the rules for mutation:
Within that scope you can assign the variable a value of the same type or more derived as long as the invariants at that point aren't broken. Under the hood we'd have to reassign every alias up to that point, I guess.
You can also assign things of a wider type (anything assignable to the original variable). This does not cause an implicit narrowing conversion. Instead, from that point it's illegal to use that variable in a manner which relies on the type guard having succeeded. That's where flow analysis comes in. So even if you re-assign an
Object
variable which has been promoted to aString
variable with anInteger
value you can still use it like anObject
. It's just that any code which used it like aString
, including overload resolution, type inference, member accesses, etc, will report an error.This way flow analysis doesn't have to feed type information back into initial binding. It sort of works on the idea that the
Object
alias ofobj
gets re-assigned, but theString
alias ofobj
becomes unassigned. So flow analysis just tracks reads ofString
that are unassigned. In theory one could reassign theString
alias ofobj
to fix this. And any usage ofobj
and anObject
(e.g. by calling members ofObject
or implicit widening conversion) really reads from theObject
alias so doesn't count as a read from unassigned.The solution in this situation is either to remove the write to the variable, re-guard the code that requires obj to be
String
, or explicitly cast obj toObject
. While all of those workarounds seem ugly they're also the only legitimate code to write in those situations.This idea that flow analysis reports an error rather than "downgrading" the type is super important to avoid silently changing the meaning of code with shadowed members:
What about
Goto
s?The same asignment analysis applies. If the reference is reachable at a point where the alias is unassigned an error is reported and the same solutions apply:
Does an assignment cause re-inference if a narrower type is assigned?
That would be madness. We should discuss it!
What about
Select Case
on type?I've always thought of the principle function of
Select Case
being to use the same "left" operand for multiple tests without repeating the name over and over. So ifTypeOf
is the operator, the natural syntax forSelect Case
would look like applying it multiple times.Or
The advantage of the first form is it has a little less repetition of the
TypeOf
keyword and reads very straightforwardly--"What's the syntax in VB for doing a Select Case on the type of an object?"Select Case TypeOf obj
.The advantage of the second form is it doesn't put
Select Case
into any special mode and so you can still use all the other kinds ofCase
clauses in the same block. I don't know how often that's actually a scenario though.Both forms reuse a concept already in the language (
TypeOf
) and don't add a whole new thing (Match
) for a common scenario. In a lot of ways theCase s As String
design was a consolation prize to semantics like this.How would this work in the IDE?
I imagine we'd use a slightly different classification to indicate that the variable is "enhanced" at that point in the program. So let's say your identifiers are black by default, in a region where the type has been re-inferred it'll be purple. Then, if you loose the enhancement somehow it'll go back to black. Maybe if you hover over it the quick type will say something like "This variable has been enhanced with
String
type and can be used like a string here." or something.Summary
I think this is the most "Visual Basic" feature ever! It's very "Do what I mean" and is fairly intuitive. The last time a developer asked me why when he checks the type it doesn't automatically get that type and I sat down to write a whoe blog essay about all the technical reasons that won't work and for VB, as much as we can, it's nice to avoid a first-time programmer needing to read an essay from some compiler nerd about threading and overload resolution and shadowing (like what are any of those things?) to explain why their very reasonable intuition doesn't work.
And this is nothing particularly innovative or out there; this is actually how TypeScript and other languages work already.
I also like the idea of rehabilitating the very readable
TypeOf
operator which I've felt has suffered a lot since the introduction ofTryCast
. It's likeTypeOf
is so self-explanatory but we have this sort of inside baseball gotcha that "Ah-ha, FxCop will tell you that reallyTypeOf
uses theisinst
instruction which pushes a casted value on the stack and checks it for null so doing acastclass
after that is really just casting twice so you shouldn't do it and instead you should use theTryCast
operator and check for null for performance or FxCop and people on forums will laugh at you--THEY'RE ALL GOING TO LAUGH AT YOU!". From the same folks who brought you "Ah-ha! Lists start with 0 here because of pointer arithmetic :)"The text was updated successfully, but these errors were encountered: