-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Proposal: Block parameters #589
Comments
I don't really see what benefit this provides other than trying to make C# smell more like Swift. To me the proposed code doesn't read any better, and if anything it's harder to discern what is a parameter to the method vs. a keyword in the language. |
As far as I can tell, the only difference in your examples from lambdas is that these "blocks" don't have |
@HaloFour I'm not at all familiar with Swift, so I can't comment. The treatment of parameter names as language keywords is intentional, so I'm not surprised to see that called out. @MgSam The biggest difference happens when there are multiple block parameters and each block is therefore named. Readers unfamiliar with the library don't then have to guess what each lambda does. See the |
Swift has what they call "trailing closures" where a function that takes a lambda as the final parameter may accept that lambda outside of the function call: reversedNames = names.sorted { $0 > $1 }
// same as
reversedNames = names.sorted(by: { $0 > $1 } ) This proposal seems very similar to that, conceptually.
Why isn't named parameters sufficient here? |
I think Ruby has them as well. As far as I remember, one important feature is that control flow statements inside a block work with the surrounding method, not with the block itself as a lambda: int Foo()
{
int x = MyBlockFunction {
if (false)
return 2; //returns from Foo
else
goto bar;
}
bar: return 3;
} |
@theunrepentantgeek Even in the case of multiple arguments, the only additional difference between named parameters in C# vs your proposed block syntax is a comma and a colon. I don't see any added expressiveness by your block proposal. In fact, I think your examples of C# 7.0 do a great job of showing why it's not needed. |
// Equivalent code in C# 7
Using(var connection = new Connection(),
() =>
{
// do something
}); This is not actually valid C# 7, you can't declare variables like that. Actual implementation of Using(new Connection(),
connection =>
{
// do something
}); |
Haha, that's what the Async.Using looks like that I'm using for real over here. 😄 |
@HaloFour @MgSam I'll freely admit that named parameters do come close - I've used them in this way - but I don't think they hit the mark, for these reasons:
|
I like it, and can see a number of places where I would have used it in the past if it had been available. The comments (above) critiquing the suggestion are only saying what the proposal states quite clearly at the start. Its syntactic sugar. That's not automatically a bad thing. If syntactic sugar was always a bad thing, we'd be writing everything in Lisp. |
@JohnRusk Syntactic sugar is not a bad thing. Syntactic sugar that just moves some symbols around a bit and doesn't really accomplish anything new is a bad thing. |
@HaloFour Indeed, there are a lot of similarities with trailing closures from Swift. Thanks for the pointer. One difference (based on a some quick research, please tell me if I'm wrong here): it seems that Swift only allows one such closure - this proposal allows for multiple blocks, using the parameter names as pseudo-keywords. |
How does adding a new syntax for the same thing solve this? If I like short function calls and intermediate variables, I'll probably keep using them, even if this feature was an option.
You could create an analyzer that enforces named parameters in some cases. If your developers use a style you don't like, you should educate your developers, you don't need to change the language for that.
You can usually configure those tools to disable some of their analyzers. Again, you don't need to change the language just so that ReSharper stops bothering you. |
@svick Really appreciate your comments.
Specifically, because the block syntax directly supports multiple lines of code in a way that fits in with the rest of the language. I believe that a developer who dislikes parameter calls that run over many lines might object to this: retryPolicy.Perform(()=>
{
using (var connection = CreateConnection())
{
var q = ...
return connection.RunQuery(q);
}
}); while being quite comfortable with this: retryPolicy.Perform
{
using (var connection = CreateConnection())
{
var q = ...
return connection.RunQuery(q);
}
}; Exactly because the later syntax looks much more like something built into the language. |
@orthoxerox It's interesting that in Ruby the control flow statements apply to the outer scope; I'd specifically intended this proposal to be purely some syntactic sugar for lambda expressions, which would scope things like |
A friend just forwarded me an interesting link that's related to this proposal: Fantastic DSLs and where to find them The article is a discussion on creating DSLs in Kotlin that references a very similar feature to this proposal. It seems the Kotlin feature is pretty similar to the Swift one already noted by @HaloFour on May 17th; both of them allow for a trailing lambda to be expressed outside of the normal parameter list. The "Droid" and "HTML" DSLs from that article would be possible in C# with block parameters. It's possible the other DSLs discussed would be as well - but my Kotlin isn't up to the task of judging. |
Closing this to reduce clutter in the repo. |
Background
Many enhancements made to C# qualify as syntactic sugar, improvements to the syntax of the language that improve clarity and expressiveness. Recent additions include expression oriented member bodies, and var out parameters, while earlier additions include extension methods and the null coalescence (
??
) operator.Problem
Framework and library authors often require code to be structured in a particular way. This occurs more frequently in libraries that express strong opinions about how code should be written. Examples include testing frameworks, state machines and workflow engines.
With the syntax available in C#7, it's often possible to declare these structures as wrapper methods that accept lambda expressions for the different pieces of the workflow. However, the syntax for using these involves a non-trivial amount of ceremony and the results are often only readable by those already intimately familiar with the library.
Proposal
Allow the passing of code blocks (delimited by '{' braces '}') for delegate parameters marked with the new context sensitive keyword block.
This syntax would be most useful to the library and framework authors mentioned above, giving them the ability to define constructs that look almost like they're built into the language.
)
) of the method call, but ahead of the terminating semicolon (;
).Action
,Action<T>
,Func<R>
and so on), and only at the end of the parameter list (thus maintaining parameter order when reading the code).(
brackets)
of the method call may be omitted (similar to the way object and collection initialization works).The following is a series of examples to demonstrate how this feature might be used to improve the readability of C# code.
Example: Using IDisposable
In it's simplest form, block actions would easily recreate the functionality of the existing using clause:
Note that the closing
;
statement terminator is retained to make it clear where the function call finishes. As discussed in #496, skipping the terminating semicolon would limit future options for syntax improvements and introduce ambiguity into the language.Admittedly, recreating using is pretty trivial - but it gets much better from here.
Example: ISmartDisposable
Sometimes developers want something a bit like
IDisposable
but with the addition of exception awareness. In a July 2005 blog entry, Oren Eini wrote of his desire for anISmartDisposable
interface, much likeIDisposable
but with the addition ofvoid Dispose(Exception)
. With block actions, we can easily implement this:Example: Telemetry Timing
More usefully, consider writing a static method to capture out of band timing information about an operation as a part of a telemetry package:
Multiple blocks
When multiple arguments are modified with the block keyword, each would be named (following the precedent already set for try..except..finally blocks). The name for each block would be the parameter name specified in the original declaration. Since parameter names are conventionally camel case, these would largely match the style of existing C# keywords, especially where the name is a single word.
A test framework could include direct support for the popular arrange-act-assert style:
At this point we can start to see how the proposed syntax is cleaner and easier to read. Observe that the C#7 version without named parameters is much harder to read because the lambda expressions are not named; only developers already familiar with the code would know the purpose of each lambda expression.
Using function blocks
A library that defines retry policies would use a block function to perform the action that might need multiple attempts. If the block succeeds, returning a value, the
Retry()
method would return that value. If an exception occurs, the function would be retried multiple times until either the retry count was exhausted, or a value returned.Blocks with parameters
Block actions of type
Action<T>
orFunc<T>
would allow a parameter to be passed into the appropriate block by reusing the syntax already present for try..catch, starting the block with a declaration of the expected parameters. Contravariance would be in play, allowing the block to be declared with a more generic parameter than expected.A declarative UX framework could make the validation for a new field in a dynamic data entry form very declarative:
Optional block parameters
Just as regular
Action
orFunc
parameters can be made optional by giving them the default value of null, block parameters should behave the same way.Transactions (whether for working with databases or other transactional systems) could be managed with explicit support for both rollback and compensation, useful for systems that need to undo changes in one system when a transaction elsewhere fails.
Since the declaration includes only block parameters, the brackets following the method call are elided. This is similar to the way brackets are optional when using object and collection initialization syntax.
The text was updated successfully, but these errors were encountered: