-
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
Top-level statements and functions #3117
Comments
See relevant discussion at #2765. |
The primary thing that bothers me here is scoping. i.e. both around 'args' as well as the scopes of variables introduced in teh statements that precede a namespace. However, it may just be an initial aversion that i coudl get over. Given that your goal is -- another issue for me is that while this is pitched as 'simple programs' it seems to still allow the statements to coexist with namespaces/classes. First, this isn't really 'simple' to me anymore. Second, it actually opens up large cans of worms for me. For example, if i were to be able to have top-level statements that can be in scope for the rest of my program, then I absolutely would want to be able to make those top level variables I strongly like the idea of simple-programs. But I actually don't think this goes far enough. Perhaps a simple program should only be top level statements/local-funcs? |
Where do imported namespaces come in? Or do we need support for It's difficult to not have an immediate negative visceral reaction to this proposal. It feels like it creates yet another dialect of the language without solving for any problems or the use cases suggested. You wouldn't be able to take CSX and run it this way, not without additional syntax work. You couldn't use most of the language as you'd expect. All variables end up in some mixed global scope. Feels like tools like LINQPad already satisfy this need and do so in a vastly superior manner. |
@CyrusNajmabadi I agree that simple programs should remain simple. I am not even convinced that splitting your simple program into multiple simple files is something desirable. I imagine that Of course, the scripting dialect could invent its own way of including other files, but that's outside the scope of this issue. |
I agree with @MadsTorgersen proposal, that this feature should be targeting simple programs and people learning the language (and should be a shortcut for what goes inside Main()). It could be worth adding a little more detail (with examples) to the section on the scoping of local variables and functions just so everyone is clear. |
I'm feeling a little bit sceptical of this proposal. Writing a static Main function is not particularly difficult, and the tooling generates it for you anyway. In terms of benefits of top level statements I would suggest there's almost none from an actual use case perspective. Instead I feel this is more of a marketing issue. C# looks old and stuffy because you need so many things to create an app. Python you just type something and it runs. Marketing is important, but I don't think it's worth introducing a whole load of complexity for it. Instead I would keep this extremely simple. You can have a single file in a project with top level statements, which act exactly as if they're inside an async Main method. They are not globally scoped, and can't be referenced anywhere else. That should be enough to give beginners their python feel. |
For my part I really like Scenario 2. That would allow for significant time savings via less typing. And having worked with VB.NET for a number of years, I do miss VB's modules and their implicit imports.
I would love to have globally accessible implicit and other operators I could define even for existing CLR types. So if the team can find a way to include those it would add even more value to Scenario 2. |
@YairHalberstadt I would absolutely use simple C# programs for throwaway scripts and tiny utilities. |
@orthoxerox |
@YairHalberstadt one file per utility/script. I've already written I don't think it's nesessary to support multi-file "simple programs". |
If all this proposal does is take the statements in the file(s) and put that into the middle of a generated |
@HaloFour It's a bit more complex than that, since the tool has to recognize the usings as well. |
I see nothing in this proposal that claims that IMO, we'd get more mileage making it easier to get/use CSX, and adding support for converting CSX scripts to a C# project. |
For me, the simple programs I would want to write:
This means that for me, the suggested ordering of "statements right before the namespace_member_declarations" would not be natural, I'd prefer it the other way around. And being able to have top-level local functions in other files and being able to access top-level local variables from other files is unnecessary and probably undesirable. On the other hand, the suggestion by @CyrusNajmabadi of having only "top level statements/local-funcs" goes too far: being able to declare types is necessary for the "simple programs" I want to write. |
Very interesting point @svick . Thanks! There's something definitely more appealing to me about that as it feels much more 'natural' in terms of scoping. i.e if i think of the 'top level locals' i declare as similar to 'method locals', then placing them after everyting else feels 'better' (since locals can't be referenced by code that is earlier than their declaration). |
@HaloFour Take a look at the grammarlet Mads posted:
Usings are supported, and they must precede the statements. I actually don't mind if CSX is merged with mainline C#, I can then just treat this proposal as the first step in that direction. |
Would like to voice that I prefer the scenario 2 specifically Is it true that I could assume that top level function will be Another main reason I really support this feature is that, outermost Also I would like to repeat myself that, I wish that we could not write top level statement. Only function and property. Because, unlike class that easily make dependency chain, we cannot expect timing and order of the statement aside from |
Scenario 2 is also my preferred option. But I disagree with @MadsTorgersen suggestion of:
|
I think that would be very useful to support an FP rather than class based style of programming. Each file becomes a sort of module and defines a number of private, internal, and public functions. |
I don't understand the use case here. Is something wrong with 'csx' scripts? Why can't 'learners' and those who want 'simple programs' just use the already-existing scripting dialect? If the reason is "no one is using it"- that's a tooling and education problem, not a language design problem. You guys released CSX and then did close to zero promotion of it- to my knowledge, no one official has ever blogged about it or demoed it at conferences, and development on C# Interactive stopped almost as soon as it started. |
@CyrusNajmabadi I agree. IMO, having both top level statements/local-funcs and "regular" C# code coexist as proposed here can work, but allowing them to mix might be against the simple program scenario that motivated this proposal in the first place. However, this would pretty much make it identical to a csx, right? It seems to me that making improvement to existing C# scripting would address the simple program scenario more effectively. |
@MgSam The motivation here is to come up with a single dialect where you wouldn't have to think about the environment regardless to proficiency or task, people who just want/need to write a "simple program" and run it shouldn't use a different tool just because now they want to define a top-level function or whatever but for beginners this might be a show stopper especially for new programmers with no prior knowledge and this barrier is a language concern because there are two different dialects and two different tools for the same language. |
@AlgorithmsAreCool With all due respect your comment makes zero sense here. |
@eyalsk In retrospect, I agree. I've removed it. |
Does it really solve that problem, though? Sure, you might get "Hello World" off the ground faster, but for anything even slightly less trivial you're going to have to jump that same hurdle. And does that hurdle really exist, even for beginners? Odds are that a beginner is starting from a tool like Visual Studio or If adoption and outreach are the goals I'd suggest that there are significantly better opportunities that don't result in creating dialects of the language. And if the Tiobe index is to be trusted it doesn't appear that C# is having too many issues attracting developers. Neither do most of the other languages towards the top of that list most of which require some kind of syntactic boilerplate. Oh, and bring back temporary solutions in Visual Studio. The removal of that feature has been infinitely more annoying to my ability to toss together a quick&dirty project than having Visual Studio automatically generate some code around the code I want to write. |
In the eye of beginner programmer, even the word Starting with |
Scripts I made or was confronted to may often fit in the 1st "Simple programs" category, but with source code split under multiple files. |
@333fred What does mean |
Correct. See https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-09-28.md#top-level-functions. |
@333fred It is very disappointed ... (((((((((((( |
We did. That was part of our decision making process. But we rejected this proposal as we didn't think it was the right direction for the language. |
@CyrusNajmabadi I think to make right decision for this feature is to create a poll and ask people will they use this feature and would not be it annoying that exist two solutions: static methods and free functions ... |
@333fred @CyrusNajmabadi C++/Swift has both top-level functions and static methods and it is not annoying anyone: https://docs.swift.org/swift-book/LanguageGuide/Methods.html |
Both of those languages have had top-level functions since their inception. |
The language direction is not decided by polls. It has a design group that has total control over the design process here.
What C++ and/or Swift do isn't part of our decision making process for C#.
'annoying' was not part of the decision making process. We genuinely do not believe this is a good feature to add to C# at this point in the lifecycle of the language. |
@CyrusNajmabadi |
It is entirely about that. The language design group chooses which features are added to the language based on their personal beliefs and feelings.
The features we decide on the for the language are not chosen based solely on their technical terms.
Yes, that feeds into our decision making. We are aware of what would be enabled by this, and we made a determination if this would be an overall positive for the language. We decided it would not be, so we rejected the feature. |
@redradist This tangent doesn't seem to be going anywhere. If you would like to discuss this further, we have several conducive avenues for that. Please consider gitter.im/dotnet/csharplang or discord.gg/csharp (#roslyn channel). |
@RUSshy as above: if you'd like to discuss this further you can do so at gitter.im/dotnet/csharplang or discord.gg/csharp (#roslyn channel). This language feature has already been decided upon. The disappointment of people was factored into the decision making process here. We assessed the pros and cons of this and determined this was not something we felt was valuable enough to make it int the language. |
Reading LDM and then I still don't think it was the right direction of decision. I don't think we need to care about BCL at all The first thing we would do with top level function is naked If there is any concern about collision I think it would be the same as introducing new function in static class then use As I was proposing. We can treat this feature as just automatic assuming default namespace and default class name of the assembly and just transpile at the compile time Actually, this feature real usage are most likely internal function so it just add convenience to development time. But we are less likely expose it, we would always have I think this would be another wrong decision as generic from C# 1. You underestimate our real usage |
Ok 👍
Perhaps. But that's life. We have to use our best judgement and make a call. That call was made here. I get that you don't like the decision we made. But nothing had changed that would cause us to reassess that. -- And, as above: if you'd like to discuss this further you can do so at gitter.im/dotnet/csharplang or discord.gg/csharp (#roslyn channel). |
Any further discussion on this topic will need to go to gitter.im/dotnet/csharplang or discord.gg/csharp (#roslyn channel). Thank you. |
This comment has been minimized.
This comment has been minimized.
@Thaina Any further discussion on this topic will need to go to gitter.im/dotnet/csharplang or discord.gg/csharp (#roslyn channel). Thank you. |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
The decision making process is not done in a measured or mechanical fashion. This entire meta discussion on how people want the language designed is not useful here. The decision has been made on this issue. The reasoning has been explained. If you want to start a different discussion on how the language is designed, feel free. However, it will remain off topic on this issue. |
Top level statements and functions
There are at least three somewhat conflicting scenarios around allowing statements and/or functions to be declared at the top level of program text.
First I'll consider each in turn, and point out how they conflict with each other. Then I'll propose an approach for C# to take.
Scenario 1: Simple programs
There's a certain amount of boilerplate surrounding even the simplest of programs, because of the need for an explicit
Main
method. This seems to get in the way of language learning and program clarity.The simplest feature to address this would be to allow a sequence of statements to occur right before the namespace_member_declarations of a compilation_unit (i.e. source file).
The semantics are that if such a sequence of statements is present, the following type declaration would be emitted:
This would make it an error for multiple compilation units to have statements, because you'd get two classes with the same name
Program
. If the assembly is run, it would also be an error to have other valid entry points, such as explicitMain
methods.Scenario 2: Top-level functions
C# restricts named functions to being declared as members of types, as well as local functions. The closest you can get to a notion of "global" (or "namespace-global") functions is to put them as static members in a class
C
and then sayusing static C;
in places where you want to use the functions directly without prefixing with a class name. This adds ceremony to both the declaring side and the consuming side dealing with the classC
.The simplest feature to address this is to add function declarations to namespace-member_declarations - the kind of thing you can declare globally or in a namespace.
The functions would be limited in the modifiers that apply: They cannot be
abstract
,virtual
,override
orsealed
. Their accessibility, like that of top-level classes would beinternal
orpublic
, withinternal
being the default.There's a design decision as to which kinds of function member declarations would be allowed: methods are key, but properties, indexers, etc. could also be considered. You could even consider stateful members (fields, auto-properties), and you would essentially get global variables. User defined operators and conversions are probably completely off the table, though, as they have relationships with their enclosing type, and there wouldn't be one.
On the consuming side, the top-level members would be direct members of the namespace, just as top level types are. If the namespace is
using
ed, or is the global namespace, the members are directly in scope.The implementation would be that a partial class is generated to wrap the members as static members. The class name would probably be unspeakable, and would be chosen in such a way that the same name wouldn't be used twice in the same namespace across different assemblies. If any of the top-level members are public, the class would be public, and marked in such a way that a consuming assembly would know to expose the members directly.
Scenario 3: Scripting
There is currently a "scripting dialect" of C#, where top-level statements and functions are not only allowed, but are the way the program is specified. It's similar to scenario 1, except that the statements are freely scattered among type declarations. (Namespace declarations are currently not allowed in scripting, but that may change in the future.)
The execution of a script is often performed by a "host", that is able to put specific things into scope of the script, as well as access the state of its "local" variables. This is enabled by the state being represented as instance fields of a generated class, of which the running script is an instance.
Also, scripts can be executed as individual "submissions", one after the other, with subsequent ones being within the scope of their predecessors' declarations, modulo shadowing. In this mode submissions need to be captured as objects, and cannot allow stack-only things such as ref variables. Similarly, scripts are implicitly
async
so thatawait
can be used freely, and this also limits the use of certain features.If we want to add top level statements and functions to C#, we don't want them to conflict with how those work in scripting. Rather we want to compile them in the requisite manner when necessary, but unify on the semantics of the features. This doesn't fully eliminate the scripting dialect, as we would still need to deal with the special directives and "magic commands" that it requires, but at the minimum we do need to avoid the same syntax to not mean materially different things.
Problem
The main conflict between these three scenarios is in how top-level functions are construed. Are they "local-to-the-main-program" functions (as in 1 and 3), or are they top level library declarations just like types (as in 2)?
If the former, then top-level functions can only occur as part of a top-level program. They can see the local variables of that program, but they (and the local variables themselves) aren't visible to e.g. adjacent type declarations.
If the latter, then top-level functions can occur everywhere top-level type declarations can occur. They wouldn't be able to access the state of a top-level program, unless we also interpret the "locals" of such a program as top-level "global" variables. The functions - as well as such global variables if we choose to embrace them - would be members of their namespace, visible to any code in the assembly, and, if declared
public
, to any other assemblies referencing it.Proposal: Simple programs
You can squint and imagine a merged feature that serves all the scenarios. It would require a lot of design work, and some corners cut. I do not propose that. Instead I suggest that we fully embrace scenario 1, essentially fleshing out and slightly generalizing the feature sketched out for that scenario above.
The primary goal of the feature therefore is to allow C# programs without unnecessary boilerplate around them, for the sake of learners and the clarity of code. A secondary but important goal is to not introduce a fundamental conflict with scenarios 2 (which we may want to revisit in the future) and 3 (not having the meaning of top-level code differ between "program" and "script" scenarios).
It should be relatively straightforward to ensure that, while more restrictive than scenario 3, for programs that are allowed, the semantics will be approximately the same; enough so that the two don't materially conflict.
The approach more fundamentally clashes with scenario 2, and in its straightforward form it would bar us from extending the feature to embrace scenario 2 in the future. I propose that we build in additional restrictions to keep that design space open.
(If we later find that there's a need for libraries of top-level functions, we can also consider an equivalent to VB's modules, which still provide a named wrapper for static members (similar to a static class), but put the names of those members in scope implicitly when the enclosing namespace is
using
ed, instead of requiring an explicitusing static
).Syntax
The only additional syntax is allowing a sequence of statements in a compilation unit, just before the namespace_member_declarations:
compilation_unit : extern_alias_directive* using_directive* global_attributes? statement* namespace_member_declaration* ;
In all but one compilation_unit the statements must all be local function declarations.
Example:
Note the use of
return
as a top-level statement. We may find that this looks/feels wrong since it's not visibly inside a body of a member.Semantics
If any top-level statements are present in any compilation unit of the program, the meaning is as if they were combined in the block body of a
Main
method of aProgram
class in the global namespace, as follows:If any one compilation unit has statements other than local function declarations, those statements occur first. The order of statement contributions (which would all be local functions) from other compilation units is undefined.
Warnings about missing
await
expressions are omitted.Normally collision between multiple
Main
method entry points is only diagnosed if and when the program is run. However, we should consider forbidding anyMain
methods suitable as entry points to coexist with top-level statements. Or if we do allow them, we should not allow synchronous ones to silently take precedence over the async one generated from the top-level statements. That precedence was only reluctantly allowed over asyncMain
methods for back compat reasons which do not apply here.The example above would yield the following
Main
method declaration:Scope of top-level parameters, local variables and local functions
Even though the
args
parameter and top-level local variables and functions are "wrapped" into the generatedMain
method, they should still be in scope throughout the program, as if they were declared with internal accessibility in the global namespace.This could lead to name collisions, ambiguous lookups and shadowing of imported names. If one is picked by name look-up, it should lead to an error instead of being silently bypassed.
In this way we protect our future ability to better address scenario 2, and are able to give useful diagnostics to users who mistakenly believe them to be supported.
LDM notes:
The text was updated successfully, but these errors were encountered: