dotnet / vblang

The home for design of the Visual Basic .NET programming language and runtime library.
290 stars 64 forks source link

Method cascading (like Dart) -- enables Fluent interfaces without any extra effort #245

Open bandleader opened 6 years ago

bandleader commented 6 years ago

obj.Method() -- returns result of Method() obj..Method() -- discards result of Method() (if any) and returns obj 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

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...

ericmutta commented 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 👍

reduckted commented 6 years ago

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.

bandleader commented 6 years ago

@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.

bandleader commented 6 years ago

@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:

If necessary, let me know and I'll provide examples later.

reduckted commented 6 years ago

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.

bandleader commented 6 years ago

@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.)

xieguigang commented 6 years ago

@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
bandleader commented 6 years ago

@xieguigang Thank you for your comment. The With construct, while well-known and useful, IMHO does not replace a true method-cascading operator:

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.

xieguigang commented 6 years ago

Place all of the method cascading in one single line is not recommended, as it:

bandleader commented 6 years ago

@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 😃

bandleader commented 6 years ago

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()

Great Fluent class. However:

  1. I had to explicitly design my class to be Fluent. I converted every Sub into a Function that returns MailerMessage, and added Return Me to the end, as well as every time I wanted to Exit Sub, etc.
  2. I had to create a method for every single property! See the Subject method above. With method cascading, I could simply use ..Subject("Subject here") and method cascading would use the already-implemented property.
  3. The class may have been written by someone else, with no Fluent API, and over which I have no control. With method cascading, I could still use his API in a Fluent manner.
  4. In fact, I could perhaps have used 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.

AnthonyDGreen commented 6 years ago

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.

bandleader commented 6 years ago

@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