Open bandleader opened 6 years ago
@bandleader Benefit for class consumers -- ability to use Fluent-style access on classes that have not been coded for it.
Hmmm...is that really possible? In my experience with fluent-style classes, they are specifically designed to "accumulate" state then do something only at the end (e.g when you call something like a build
function). For example this AlertDialog.Builder in Android: you would chain a bunch of setXXX() methods and at the end call [create()](https://developer.android.com/reference/android/app/AlertDialog.Builder.html#create()).
The only classes I can think of that are NOT specifically designed for fluent-access but can still be used in a fluent way are collection classes (e.g. using the syntax you suggested you would do list..Add(1)..Add(2)..Add(3)
...which would work but is not as easy as using collection initialisers: New List(Of Integer) From {1,2,3}
or simply list.AddRange({1,2,3}))
In any case, this would require more flexibility in placing newlines before/after operators (something we were talking about in #232), to avoid long method chains scrolling off to the right edge of the screen 👍
In my experience with fluent-style classes, they are specifically designed to "accumulate" state then do something only at the end
Yep. Just look at any of the dependency injection container registration methods for an example. Using Autofac as an example, the ContainerBuilder
class has a Register
method. That returns a different type of object that lets you specify how to register the type, and calling methods on that object returns another type that lets you further configure the registration. It wouldn't make any sense to have all of those methods available from the ContainerBuilder
.
@bandleader, do you have a real-life example of where this feature would be useful? That might help us understand the benefits of this.
@ericmutta Depends. 90% of the time, the Fluent methods directly mutate state inside the object, instead of returning a new object. (I'm sure this is the case in the Android dialog builder that you linked as well.) Then they simply Return Me
(or return this;
) so that you can continue accessing the object (calling .Create()
, or setting more properties. In these cases, the entire existence of all the setter methods is unnecessary, when simply setting properties directly would be fine, if we had a ..
operator to enable doing it in one line.
In 10% of cases (i.e. LINQ), the Fluent methods don't mutate their object, but instead return a new object. This is so you can compose them outward and retain the original in a different variable so you can compose it in a different direction. However, in these cases, the class consumer doesn't need/want this method-cascading operator, as you want the reference returned by the method. As well, the class writer doesn't need our operator, because in any case he is defining some special functionality and returning a new object -- an obvious choice for a method. This is no different then myString.Trim().Substring(1,3)
.
Rather, this operator is concerned with the 90% of cases (in my experience) when the entire Fluent method is only boilerplate for setting a property and returning the object so you can continue: Function SetName(name As String) As MyObject: Me.name = name: Return Me: End Function
which could be eliminated (the entire Function SetName could be removed) if our operator provided access to the name
property without breaking the chain.
Again, this isn't a novel idea, this is already there in Dart and Smalltalk.
Happy to answer more questions as needed.
@ericmutta In any case, this would require more flexibility in placing newlines before/after operators (something we were talking about in #232), to avoid long method chains scrolling off to the right edge of the screen
This wouldn't "require" newline flexibility any more than "regular" method chaining would. Indeed, the flexibility here would help readability, both for ..
and for regular .
method chaining, but it's a discrete improvement. Currently I use fluent interfaces all over my code, and either have long lines, or simply use the line continuation character _
. (In modern VB another option is to end your line with .
and continue with the method name on the next line, but I prefer _
.)
@reduckted do you have a real-life example of where this feature would be useful? That might help us understand the benefits of this.
I have many examples, I'll try to provide them later:
SmtpClient
, but even manually instantiated Windows Forms controls... anything. If necessary, let me know and I'll provide examples later.
90% of the time, the Fluent methods directly mutate state inside the object, instead of returning a new object.
Yes, that's often how it's implemented, but the important point is that the method that just returns Me
has a return type of a certain interface that is usually a different type. For example:
Interface IBuilder
Function Register(t As Type) As IRegisteredTypeBuilder
Function
Function SetName(name As String) As MyObject: Me.name = name: Return Me: End Function
Can you provide an example of how you would set a property using this new syntax that you're proposing?
Edit: Ah, is the first point under "Properties" in your opening statement how you are proposing to do it? I suspect that would end up being ambiguous if we allow implicit line continuations before a period.
@reduckted but the important point is that the method that just returns Me has a return type of a certain interface that is usually a different type.
Point taken -- in that case they will simply use method chaining and not use our feature (method cascading). I still do believe that method cascading still has great value for the x % of cases in which we return the object as the same type, to enable the next method call.
@reduckted Can you provide an example of how you would set a property using this new syntax that you're proposing? [...] Ah, is the first point under "Properties" in your opening statement how you are proposing to do it? I suspect that would end up being ambiguous if we allow implicit line continuations before a period.
I much prefer (for that and other reasons) the obj..Prop("value")
syntax. Much cleaner as well, and more in tune with BASIC's one-statement-per-line.
I am not bothered by the fact that Prop
above can be either a property or method.
(It's no worse than obj.fld
which can be either a field, property, or argumentless method, and also obj.method()
which can be either a Function or Sub.)
@bandleader, @reduckted
Fluent-style method cascading like d3js.labeler
Call d3js.labeler _
.Labels(labels) _
.Anchors(labels.GetLabelAnchors(pointSize)) _
.Width(rect.Width) _
.Height(rect.Height) _
.Start(showProgress:=False)
can implements by using With
keyword in VB.NET:
With d3js.labeler
.Labels(labels)
.Anchors(labels.GetLabelAnchors(pointSize))
.Width(rect.Width)
.Height(rect.Height)
.Start(showProgress:=False)
End With
Yes, that's often how it's implemented, but the important point is that the method that just returns Me has a return type of a certain interface that is usually a different type.
Using a more simple version With
syntax for implements a Fluent-style method cascading even though the target method is a Sub
(can not returns value), without change any code that you wrote before:
d3js.labeler With
.Labels(labels)
.Anchors(labels.GetLabelAnchors(pointSize))
.Width(rect.Width)
.Height(rect.Height)
.Start(showProgress:=False)
' An empty line or single line code comment will break this method cascading
@xieguigang Thank you for your comment. The With
construct, while well-known and useful, IMHO does not replace a true method-cascading operator:
With obj
and an end line End With
d3js.labeler
separately in advance and assign it to a variable. Whereas with method cascading, your entire code (instantiation, settings and all) could be written simply as: Dim labeler = New d3js.Labeler()..Labels(labels)..OtherMethods(whatever)..Start(showProgress:=False)
As far as the "simple version" you suggested: it simply removes the End With
but requires a blank line, so it's not much better -- but either way it is not BASIC-like syntax (more Python-like), and @AnthonyDGreen and the rest of the VB team will not accept that.
Place all of the method cascading in one single line is not recommended, as it:
@xieguigang Understood, but IMHO that's not a reason to force developers to split it into multiple lines, when they may have many good reasons to keep it on one line (I certainly do). If you insist, we will promise not to let it go over the edge of an A4 paper 😃
As some have requested a real-life example:
I have a Fluently-designed class called MailerMessage
-- an advanced helper class to construct email messages in both HTML and text formats, send them using a separate Mailer
object (which uses multiple SMTP servers, etc.), and log them.
Public Class MailerMessage
Inherits System.Net.Mail.MailMessage
Private DebugDetails As String = ""
Private BodyHTML As String = ""
Private BodyPlainText As String = ""
Public Mailer As Mailer
Public Function BodyAppend(...) As MailerMessage
'Code to intelligently build the message body...
Return Me
End Function
'This method does nothing but set a property!
'...but is necessary to continue the Fluent method chain.
'There are MANY like it...
Public Function Subject(subj As String) As MailerMessage
Me.Subject = subj
Return Me
End Function
Public Function Send() As MailerMessage
Me.Mailer.Send(Me)
Return Me
End Sub
Public Function LogSuccess() As MailerMessage
'Code to log our successfully sent message
Return Me
End Sub
'...and MANY, MANY more methods (omitted for brevity) that help you
'easily add recipients, attachments, HTML, plain text, and much more.
'Each one returns 'Me' so that you can instantiate a simple message,
'send and log it - all in a single statement.
End Class
'=== EXAMPLE USAGE: (user.NewMessage returns a pre-addressed MailerMessage)
user.NewMessage().Subject("Thank you for joining GitHub!").BodyAppend("message goes here").Send().LogSuccess()
MailerMessage
, and added Return Me
to the end, as well as every time I wanted to Exit Sub
, etc.Subject
method above. With method cascading, I could simply use ..Subject("Subject here")
and method cascading would use the already-implemented property.Net.MailMessage
and not written this class at all! (At most, a couple of extension methods to Net.MailMessage
could have done the trick!)I hope this demonstrates the utility of the feature! Would love input from @KathleenDollard or any members of the VB team.
I love the idea, the big concern I have is that VB already has the .
(simple) and ...
(XML) member-access operators. Which are far enough apart right now, but adding ..
might suggest some kind of linear relationship between the three (maybe). But that might not matter in the end.
@AnthonyDGreen I'd be fine with a different operator (consistency with other languages is indeed nice, but not a huge priority at the expense of clarity).
Also, while it's an unrelated feature, I personally think that ..
is a better match for a high-precedence (short) piping operator (as I proposed in #165), i.e. a way for you to "call" any method, function, Await or a typecast on the left operand, in a Fluent manner (also solves extension methods more cleanly IMHO).
Dim ExtractValue = Function(x As String) x.Split(":").Last() 'Or this could be a method
Dim fld = FetchAgeFieldAsync() 'Will resolve to "Age: 54 "
Dim age = fld..Await..ExtractValue.Trim()..CInt '= 54
'OLD WAY:
Dim age = CInt(ExtractValue(Await fld).Trim())
'...had to backtrack 3 times while typing it,
'and has wrong execution order
obj.Method()
-- returns result ofMethod()
obj..Method()
-- discards result ofMethod()
(if any) and returnsobj
Therefore, you can use method chaining:obj..Method()..SecondMethod()..ThirdMethod()
...without having to create specific Fluent-style methods that return the object.More information at the Dart website
Benefits
Setting properties
object..Property = "value"
and then on the next line..NextProperty = "value"
...
at the beginning of a line continues the previous line. (In any case these days VB's allow unfinished statements to implicitly continue onto new lines. This would be the same thing the other way around.)object..Property("value")..Property("value")
. It isn't as clear to the newcomer that this is a property assignment, but it does preserves the one-statement-per-line rule. (Parameters for parameterized properties would be in the parentheses before the new value.)Real-world example of where this would be beneficial
Example class in my comment below.
Symbol/syntax for this feature
Thought Dart uses
..
and it looks nice and clean, we were discussing perhaps reserving..
for the Pipe-Forward operator (#165 - and related functionality - see #154). If so, we would need to find a different symbol for this. Maybe.<
to show that the expression will return what's on the left side?New idea: I am thinking that perhaps we could put the cascaded method call in parentheses:
obj.(Method).(MethodTwo(5))
. This way, 1) it looks like a regular method call, 2) the syntax expresses visually the concept that the method is being called "parenthetically" but its result is not used in the expression. There is also no amibiguity because.(
is not permitted anywhere else.Thanks for such a great, powerful and yet friendly language. Looking forward to the discussion...