Join GitHub today
GitHub is home to over 40 million developers working together to host and review code, manage projects, and build software together.
Sign upTop-level statements and functions #3117
Comments
This comment has been minimized.
This comment has been minimized.
|
See relevant discussion at #2765. |
This comment has been minimized.
This comment has been minimized.
|
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? |
This comment has been minimized.
This comment has been minimized.
|
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. |
This comment has been minimized.
This comment has been minimized.
|
@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. |
This comment has been minimized.
This comment has been minimized.
|
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. |
This comment has been minimized.
This comment has been minimized.
|
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. |
This comment has been minimized.
This comment has been minimized.
|
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. |
This comment has been minimized.
This comment has been minimized.
|
@YairHalberstadt I would absolutely use simple C# programs for throwaway scripts and tiny utilities. |
This comment has been minimized.
This comment has been minimized.
|
@orthoxerox |
This comment has been minimized.
This comment has been minimized.
|
@YairHalberstadt one file per utility/script. I've already written I don't think it's nesessary to support multi-file "simple programs". |
This comment has been minimized.
This comment has been minimized.
|
If all this proposal does is take the statements in the file(s) and put that into the middle of a generated |
This comment has been minimized.
This comment has been minimized.
|
@HaloFour It's a bit more complex than that, since the tool has to recognize the usings as well. |
This comment has been minimized.
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
|
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. |
This comment has been minimized.
This comment has been minimized.
|
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). |
This comment has been minimized.
This comment has been minimized.
|
@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. |
This comment has been minimized.
This comment has been minimized.
|
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 |
This comment has been minimized.
This comment has been minimized.
|
Scenario 2 is also my preferred option. But I disagree with @MadsTorgersen suggestion of:
|
This comment has been minimized.
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
|
@YairHalberstadt, I've become nervous of mentioning the "F word" around here as I seem to suffer a lot of backlash over it at times. It's not about FP v OOP for me. Sometimes, I want a deterministic free function. Sometimes I want a stateful class. I'd just like the ceremony around the former reduced and scenario 2 (with support for private functions) provides this. |
This comment has been minimized.
This comment has been minimized.
|
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. |
This comment has been minimized.
This comment has been minimized.
@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. |
This comment has been minimized.
This comment has been minimized.
|
@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. |
This comment has been minimized.
This comment has been minimized.
|
@AlgorithmsAreCool With all due respect your comment makes zero sense here. |
This comment has been minimized.
This comment has been minimized.
|
@eyalsk In retrospect, I agree. I've removed it. |
This comment has been minimized.
This comment has been minimized.
|
Great feature! i love the direction C# is taking When learning C#, even Java.. i remember the major pain was to understand why i need create a namespace and class to start the program This will solve this issue for many people who want to get started with C# That kind of issue doesn't exists for Switf/Kotlin/Rust, getting started and teaching becomes much easier, less frictions |
This comment has been minimized.
This comment has been minimized.
|
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. |
This comment has been minimized.
This comment has been minimized.
In the eye of beginner programmer, even the word Starting with |
This comment has been minimized.
This comment has been minimized.
I disagree that such syntax poses a burden to beginners, either to C# or to programming in general. They're going to have to learn so much about the syntax of C# in order to put anything anywhere anyway, and many of those concepts (like variables, definite assignment, etc.) are so much more complicated to grasp than requiring a single container around a method ( If C# is going to seriously consider the addition of top-level functions it should do so in consideration as to how they would impact and benefit developers of all skill levels and projects of all shapes. |
This comment has been minimized.
This comment has been minimized.
This might be only my opinion. But I still remember when I myself was a beginner. So this is my direct experience. I have learn only a little bit of C and start learning Java and C# at the same time. I remember I feel much burden on the It very easy when you have learn C or older language as a starting point to programming. You already know what a code of program really is. It really another story when you don't really know it but just start learning C# or Java as your first ever language in your life. And I think most of us here cannot experience that kind of experience anymore I have watch one 3Blue1Brown chapter, he talk about "This problem seems hard, then it doesn't, but it really is". He make that video about question in math competition that the organizer think it is quite easy, but the participant cannot solve it |
This comment has been minimized.
This comment has been minimized.
As for me I think this feature already benefit us if we could write only one My point is, the benefit for beginner that some people talk about is also as real and also a great bonus for our language too |
This comment has been minimized.
This comment has been minimized.
I agree with this. There is a rather significant amount a syntax a beginner would have to read and understand to know what is happening. The less this is happening, the best. |
This comment has been minimized.
This comment has been minimized.
|
I also want to voice my support for the opinion that the ceremony with classes, namespaces and especially |
This comment has been minimized.
This comment has been minimized.
This is purely subjective and a slippery slope because this doesn't apply to many of the other features introduced to C#. |
This comment has been minimized.
This comment has been minimized.
I disagree. It's been my experience that virtually every feature that has been adopted by the C# team and added into the language has had to meet this same subjective but high bar in that it must either benefit sufficient developers directly or must benefit runtime/library authors sufficiently that other developers feel that benefit. |
This comment has been minimized.
This comment has been minimized.
|
@HaloFour I agree with what you said and I don't think it's remotely close to what you said previously where you state the following:
Which I disagree with but anyway, maintaining two different dialects and two different tools of the same language is that high bar imo but we'll see, maybe not. |
This comment has been minimized.
This comment has been minimized.
Hm, I was trying to say the same thing both times. Language is hard. :/
I don't think this changes that at all. We're not talking about making CSX a part of the language, we're talking about making a third dialect where the compiler pretends that everything you're writing happens to take place within That's what I mean about the broader appeal. "Simple programs" seem to target a very small subset of users who are either first starting out and don't understand the ceremony around their code (but somehow understand all of the complicated nuance required to actually write that code), or are writing the simplest of programs that can manage to be crammed into a single method. In either case my opinion is that the work necessary to even get that far is a much bigger hurdle in that you still have to setup a project or invoke the compiler manually, and in the former the tooling can autogenerate all of the ceremony for you which, IMO, makes its removal a moot issue. My opinion is that for both newbies and experienced devs alike the ability to quickly prototype new code is served not by a compiler dialect but through an excellent REPL and sandbox environment. IMO it's not necessary for the compiler to officially support that dialect. Both JShell for Java and Ammonite for Scala support writing top-level statements directly into the REPL and neither language supports such a construct, and my experience with Scala/Ammonite is that this is not an issue for either newbies or experienced developers. The most important thing is the ability to very quickly spin up an environment into which you can bang out a series of statements interactively while following the state during the flow, not being limited to a very small subset of the overall syntax. Note that the shortest "Hello World" in CSX is literally Anyway, I think (we can all agree that) I've ranted enough about the subject. |
This comment has been minimized.
This comment has been minimized.
|
@HaloFour From the scenario detail in this proposal I think we have 2 options we need to choose
// HelloWorld.cs
System.Console.WriteLine("Hello World");
// HelloWorld.cs
using System;
void Main()
{
Console.WriteLine("Hello World");
}Personally I prefer the second case, and I don't like the first case, I too think it is unnecessary. And also I wish it would have more constraint than csx, such that it would be better seamless with our current C# But in both case it drastically reduced from current C# starting point The point of this is not just for quickly prototype (still it is a bonus too) but 1 - The real newbie, actually really totally zero experience, will see this as a starting point to learn C#. Less verbose. Less token in the eye that could be distract them. And easier to just write something before or after 2 - Experience developer, which is us all, could really start any size project from this point. Unlike prototyping or experimenting in csx, we can really start project with this first file even without This is just the benefit that this feature allow us to start the project with this file |
This comment has been minimized.
This comment has been minimized.
|
If this ever becomes more than a proposition, for what its worth, I think it would be essential to limit access to that top-level code and disallow sharing it with non-top-level code. The idea to limit top-level code to a single file seems like a good start, but it may be too limiting from a learning perspective. To have taught introduction to programming several times to multiple people having a different level of knowledge/affinity with programming, I can state that simple concepts, like writing code between However, I believe that transforming C# into a scripting-like language is a bad idea. In conclusion, from my experience, globals are your enemy when designing programs even if they first appear as friends; they almost always end up causing more troubles that good |
This comment has been minimized.
This comment has been minimized.
|
That I believe is the idea "scenario 2" above, and I can get behind that idea. It's similar to Kotlin in that the top-level functions in a file are "promoted" to static methods in an autogenerated partial class. The compiler would then automatically bring those members into scope anytime you import the namespace in which they are defined, as if you did a For newbies and prototyping alike, I want this in Visual Studio: combined with features to show the flow of execution from SharpLab like this: |
This comment has been minimized.
This comment has been minimized.
|
Some opinions-
For C# proper, I would like to have scenario 2 "Top-level functions". |


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
Mainmethod. 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 explicitMainmethods.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
Cand 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,overrideorsealed. Their accessibility, like that of top-level classes would beinternalorpublic, withinternalbeing 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
usinged, 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
asyncso thatawaitcan 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
usinged, 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
returnas 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
Mainmethod of aProgramclass 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
awaitexpressions are omitted.Normally collision between multiple
Mainmethod entry points is only diagnosed if and when the program is run. However, we should consider forbidding anyMainmethods 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 asyncMainmethods for back compat reasons which do not apply here.The example above would yield the following
Mainmethod declaration:Scope of top-level parameters, local variables and local functions
Even though the
argsparameter and top-level local variables and functions are "wrapped" into the generatedMainmethod, 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.