HaxeFoundation / haxe-evolution

Repository for maintaining proposal for changes to the Haxe programming language
111 stars 58 forks source link

Module-level functions #24

Closed nadako closed 5 years ago

nadako commented 7 years ago

Allow defining functions (and maybe vars) directly in .hx files instead of making a class with static methods.

Rendered version

Justinfront commented 7 years ago

So the proposal is to allow stuff like this?

package;
// import Main.hello;
class Main {
  static public function main():Void {
    hello();
  }
}
function hello(){
  trace('hello function');
}

Could main even be just a function if user wanted is that part of your proposal?

package;
function main():Void { 
   hello();
}
function hello(){
  trace('hello function');
}

So essentially in more general case this:

package myStuff;
class MyFunctions{
   static function hello(){
      trace('my function hello');
   }
}

could be written:

package myStuff;
// package myStuff.MyFunctions;
function hello(){
      trace('my function hello');
}

Interesting proposal if I have understood but seems kind of like icing for the user especially if it worked like below, your just kind of removing the 'class' word.

package myStuff.MyFunctions;
function hello(){
      trace('my function hello');
}

but then you could not also put a class in the file, and could you have two groups of functions in a file and import them separately surely the extra of a class makes little odds? Is it not acceptable if no constructor to allow users to drop using static accept that I guess via reflection you could still create a class so you would need different holder name.

package myStuff;
functions MyFunctions{
   function hello(){
      trace('my function hello');
   }
}

Which seems a lot of change just so that users can avoid word 'static' and looks pretty the same as now.

I think the proposal could do with samples of how code might look atleast in your prefered solution, it would make it clearer for people who struggle to read technical definitions but are fine seeing code patterns.

So would constants or variables be in these packages ?

package myStuff;
const helloText = 'hello';
function hello(){
  trace( helloText );
}
function hello2(){
 trace( helloText + ' ' + helloText );
}
nadako commented 7 years ago

Could main even be just a function if user wanted is that part of your proposal?

Yes, I mentioned that. In fact, it'll automatically work if the implicitly created class for functions will be the primary module class.

your just kind of removing the 'class' word

That's the intention - when you don't do OOP, this class wrapping becomes just a visual clutter. And it's not just the class keyword. It's also class name duplicating the module name, braces, static keyword for each function, extra indentation level etc.

but then you could not also put a class in the file

That's not true. This proposal allow for placing module-level functions/vars along with other types just fine, that's the idea.

would constants or variables be in these packages ?

it's mentioned in the proposal as well, that's one of the open questions, but i think it wouldn't hurt to support that as well.

the proposal could do with samples of how code might look

thanks, I added a sample. however you seem to have read the proposal carelessly, because most of what you're asking about was mentioned there... maybe it's because of my english though. :)

benmerckx commented 7 years ago

Allow marking those as private as well? I think TDFunction(name:String, fun:Function) wouldn't allow for that as Function doesn't have the needed access?

private function rand() return Math.random();
var MY_RANDOM_NUMBER = rand();

Edit: thinking about it metadata and docs would be useful as well.

nadako commented 7 years ago

Allow marking those as private as well? I think TDFunction(name:String, fun:Function) wouldn't allow for that

Yes, private functions should totally be supported, though it just occured to me that haxe.macro.Expr structures doesn't have a notion for other private types either, so I guess that's something that should be added to the TypeDefinition structure instead of TypeDefKind constructors.

frabbit commented 7 years ago

Maybe the easiest way to implement this is to put all top level functions as statics automatically in the module named class and disallow the definition of a class named as the module itself. This way it would only be nice sugar for this common case.

nadako commented 7 years ago

Maybe the easiest way to implement this is to put all top level functions as statics automatically in the module named class and disallow the definition of a class named as the module itself. This way it would only be nice sugar for this common case.

Yes that's what I proposed. I also propose a more complex option, but I think we should settle with this.

Justinfront commented 7 years ago

The example helps thank you. Your english is fine, code examples really are much clearer than words for some of us, even if they don't cover all the detail.

I don't really understand what frabbit said maybe a specific example would help.

Looking at the example you added, I am still not clear on how you would prefer/imagine package/import will work in practice, the example glosses over thier use, could you add an example of your prefered package and import use. For instance if I had a file MyMath, and you have a group of functions called trig, and a group called matricies in the MyMath file, and you wanted to use one in one class and one in another, what would that actually look like, or is that not supported?

I understand the proposal is for functional approaches to be more clear, but it seems it is just sugar. I don't really see it adding much beyond creating 2 ways to do one thing, which will make learning more complex overall - even if initially it's simpler, your adding a lot of new - and it all needs explaining, even something like lots of slightly different ways of doing 'if' add to the amount people need to learn before they can be productive.

The proposal could make for cleaner haxe code and make it clearer that Haxe has functional leanings, but it's hard for me to see it as more than a conceptial icing change, I don't see the bigger picture of how this is more than just reducing keystrokes maybe that's enough but not historically!

Aurel300 commented 7 years ago

Interesting. It is basically syntactic sugar, but so was the arrow notation for functions and people were pretty happy with that. Some more issues should probably be clarified:

Typically e.g. using Lambda; would look through all public static functions in the class Lambda in the module Lambda. With your proposal, does this include the classless functions? As I understand it, they are implicitly static.

// Lombdo.hx
function double(x:Float) return x * 2;
// Main.hx
using Lombdo;
static function main() trace(Math.PI.double());

Should this compile?

Can the module-level functions be declared macro? How do you use them as build macros?

Is there any way to access the module-level functions of a module with reflection? Currently the reflection and type APIs can only handle classes and instances, I don't think they have a notion of modules. Is a function declared at module level a function instance? What is its lifetime?

markknol commented 7 years ago

This would make explaining Haxe and documenting it much easier, most examples don't need classes and could run/tested this way. Making small tools would also be easier since a main function and maybe some other function can already be a full Haxe application. Its not that big deal to wrap a class around it, but if you can leave it out, why not.

My only concern is that I think you directly want module-level variables too, otherwise that will confuse ("why can I use function here, but not var here?").

Also try.haxe could remove it's "simple" mode, which now wraps everything in a class with static main function. @clemos

nadako commented 7 years ago

@Aurel300 since the idea is to place module-level functions in an implicitly created class (so they end up being its static methods), they are going to work just fine as macros and static extensions. and reflection is a good point in favor of making that class the primary module class (so you resolveClass the module name and access its statics as usual).

clemos commented 7 years ago

@markknol: on try.haxe, it's indeed just a matter of shortening code samples to a minimum, not at all about advocating for a more "open" way to define functions or variables. I must say I'm quite used to the current way (incl. with import MyStaticHelpers.*) and find it convenient as is. Actually, I think that introducing this could pollute the toplevel namespace, potentially importing a lot of (unwanted) toplevel identifiers. Also, this would add yet another complexity level to identifier resolution. Finally, I'm not sure I understand how this proposal relates to functionnal programming, other than making it easier to avoid the Java-sounding class keyword :trollface:

Aurel300 commented 7 years ago

@clemos I can see the appeal functional programming. A lot of times, I would like to do something like arr.map(flatten) or zip(arr1, arr2) or arr.map(id), etc. Lambda in std. does something similar, when you use it as a static extension. However, because it is a static extension, it will only ever be available as a method of a object, i.e. you have to access it with the dot operator. With FP-like functions it is often needed to be able to access them anywhere, without having an instance – because, after all, you might want to pass a function to a higher-order function. So compare:

arr.map(a -> a.flatten())

And:

arr.map(flatten)

Other functions, like zip, don't really make sense when accessed as a method of one of its two arguments.

So yes, importing a module with a bunch of functions like this would "pollute" your namespace, although naturally any variables or functions you define locally override the imported ones. However, there are situations when you are writing very FP-focused code and it would be beneficial (for legibility and nice syntax) to actually have them in the namespace.

fullofcaffeine commented 7 years ago

Hey guys, can we proceed with this one? I think we have enough 👍 for it to be merged.

markknol commented 7 years ago

The core team should decide here.

Pauan commented 7 years ago

This is one of the few things preventing me from switching from TypeScript to Haxe, so this gets a big :+1: from me.

Haxe provides a lot of features supporting functional oriented paradigms (most importantly first-class functions)

In addition to first-class functions, Haxe also has ADTs (e.g. enums) and pattern matching, which is very unusual for an OOP language (but definitely a good thing!)

Haxe definitely has the potential to be an excellent multi-paradigm language, supporting both OOP and functional style seamlessly. It just needs a little nudge in the right direction (e.g. this proposal).

While this proposal describes module-level functions, I think it would be logical and consistent (although less useful) to also allow module-level variables, treating them similarly static vars.

If functions are "elevated" into static methods of an implicit class, couldn't top-level var be elevated into static members of the implicit class?

I think top-level var will absolutely be needed: to define module state (e.g. a counter):

var counter: Int = 0;

public function increment(): Int {
    return ++counter;
}

Or to define static data structures which are shared between functions:

var foo = {
  // various properties defined here
};

public function bar() {
  // do stuff with foo
}
nadako commented 7 years ago

Yes, I mentioned toplevel var in the original proposal and I think it should be done as well. I also now think that it should simply become an implicitly created main module class with static fields. I'll update the proposal tomorrow to be more clear about it and maybe provide an sample implementation.

fullofcaffeine commented 7 years ago

As per your proposal, we will have a way to execute code at the module? We will be able to call functions at the module-level?

nadako commented 7 years ago

@fullofcaffeine if you're talking about arbitrary module-level expressions, then no, it's only about defining module-level functions and vars

fullofcaffeine commented 7 years ago

Fair enough, I guess module-level expressions would just overcomplicate things, since we would have to assume code would be executed upon importing modules, which could lead to unwanted side-effects. I think the proposal is good as it is.

back2dos commented 7 years ago

Haxe provides a lot of features supporting functional oriented paradigms (most importantly first-class functions), however it lacks a clean way to actually define functions without creating a wrapping class. This is annoying and gives a feeling of bloatedness to new people coming from non-OOP background. [emphasis added]

Everyone with a decent understanding of functional programming will consider nesting functions in a class as a minor nuisance at best. Even Scala requires global functions to be defined on some object. People drawn to functional programming will choose Scala over Haxe for a pretty long list of reasons, none of which is about having to type a keyword more or less.

The benefits of this proposal seem negligible to me, as opposed to the complexity added. If you want to do FP practitioners a favor, that's much appreciated, but this just ain't it ;)

ibilon commented 7 years ago

@back2dos maybe not for people coming from functional programming yeah but from imperative languages or even any language which has top level function/expression it makes porting code a lot easier. For instance from javascript.

as a minor nuisance at best

It certainly is only minor, but for people trying out haxe as an alternative among others it can be the detail that drives them away. Especially since they are more likely to try some small code, and top level function makes hello world level snipet a lot succinct.

back2dos commented 7 years ago

It certainly is only minor, but for people trying out haxe as an alternative among others it can be the detail that drives them away. Especially since they are more likely to try some small code, and top level function makes hello world level snipet a lot succinct.

People who will evaluate a language based on hello world snippets and such minute details should not be taken seriously, and certainly not be catered to. I seriously doubt we can expect any meaningful contribution to our ecosystem from them.

On the other hand long time contributors like Massive Interactive and Franco are leaving Haxe behind. Those are the people we should not be losing:

There are many measures we could take to make Haxe more appealing to frontend web devs and FP followers (two groups with a rapidly growing intersection). This is not one of them. That's no reason not to add this, but I wanted to make it as clear as I can what this proposal will not be achieving, regardless of hopes projected onto it. If the remaining benefits are considered worth the efforts, by all means go ahead ;)

Pauan commented 7 years ago

@back2dos Scala is not representative of functional programming: every other functional language does not have nested classes, instead they use top-level functions. Scala uses nested classes because it needs to interop with Java.

In addition, every non-OOP language allows for top-level vars and functions: Python, Ruby, JavaScript, TypeScript, Perl, PHP, every Lisp, C, C++, F#, Haskell, OCaml, PureScript, Elm, etc.

It's not just a "functional language" thing, it's an "every non-OOP language" thing. It's far more common for a language to have top-level vars and functions than it is to not have them.

I can guarantee you that many people from non-OOP languages (including JavaScript and TypeScript) will be very turned off by requiring functions to have an extra indentation level and static.

Those people are not useless, they are simply used to having a less verbose language, so why would they choose to switch to a more verbose language?

Adoption rates are controlled by two things: the benefits of switching and the costs of switching. There are many benefits to switching to Haxe, but there is also a lot of costs (including this issue). Removing the costs tends to increase adoption rates more so than increasing the benefits.

That's why TypeScript is so successful: it's so easy to switch from JavaScript to TypeScript. People don't care that TypeScript is a bad language. They care about how easy it is to switch.

That's not illogical or stupid behavior: switching languages is a very big deal. Learning a new language requires a huge time and energy investment, and rewriting a program in a new language is also a huge time and energy investment. Languages are a long-term investment. People are only going to switch if they think it's worth it.

Making it easy to switch to another language reduces the time and energy investment, which makes switching much more attractive.

And people aren't going to spend months evaluating whether a language is good enough to switch: they're going to make that decision quickly, usually after taking a look at some examples and the tutorial. There's so many languages out there, and so little time to choose, they can't afford to spend a lot of time carefully analyzing every language.

Catering to beginners increases adoption rates dramatically, because even people who have been programming for decades in other languages are still a beginner to Haxe. Beginner doesn't just mean "new to programming" or "bad programmer", it also means "good programmer who is used to other languages".

There are so many good languages to choose from, why would people choose a language which has a bad first-time experience? Why would people choose a language which forces them to write their code more verbosely for no benefit?

When you are writing a large program, you will have thousands of functions, and if every single function needs an extra indentation level and an extra static, that's very annoying.

Remember: because languages are a long-term investment, if somebody chooses to use Haxe, they will be stuck with the extra verbosity forever. It's not just a few characters. It's a few characters multiplied by every function, multiplied by every program, multiplied for years. It's not just a "minor" thing that only affects newbies.

nadako commented 7 years ago

@back2dos off-topic, but since I researched d.ts loading, I have to comment on this for future readers:

Consuming .d.ts files is actually much harder than parsing strictly structured low-level .net/java bytecode: TS has high-level stuff like declaration merging, type queries, type unions/intersections, literal types and so on. We would be reimplementing a big part of TS compiler, which is, unlike .NET/Java bytecode is constantly evolving. Even if we had resources to support that, automatic externs generated from d.ts would be way worse than ones that come from jar/dll. The proper way to go here is to write a d.ts-to-extern converter that uses TS compiler through its API do do the typing heavy-lifting. Then, if we make it flexible enough to be usable without changing externs by hand (much), we could look into integrating it with Haxe compiler more tightly.

nadako commented 7 years ago

@back2dos on-topic, while I was writing my previous comment @Pauan already said most of what I had in mind :) I'll just add my 2 cents here:

Justinfront commented 7 years ago

Having thought about Dan's proposal some more I am in favour, it makes lots of code cleaner and simpler. Typescript is only relevant to Haxe in terms of Angular's popularity and if Haxe manages to promote a better framework then that could disappear tomorrow. Chasing Typescript won't allow Haxe to become popular. Haxe's main problem is that still few people of heard of it let alone considered it. This change makes Haxe more approachable and more feasible for it to be taught as a first language in schools.

back2dos commented 7 years ago

First of all, let me address this whole idea that this will allow using Haxe for teaching, which is outrageously preposterous:

  1. This proposal will not, in the slightest, allow to write class-less scripts in Haxe. For that to be the case, one would actually have to be able to put arbitrary statements on the top level, which is a big deal. The point being that while trace("hello world") is a valid try.haxe snippet, it's not in and on itself a valid Haxe program. Making it so is arguably a mildly interesting idea, but it's a whole other deal than what's proposed here.
  2. Putting together a good learning environment is a tremendous undertaking. Having one that is better than JavaScript might be possible. But to think that trivial changes like these will bring us even anywhere near Smalltalk, Self, Lisp (with Light Table while you're at it), Apparatus or the gazillion of other well engineered interactive environments is outright stupid and an insult to decades of creative research into the field. A REPL would maybe be a start to get anywhere near that.
  3. The mandatory class keyword does not stop a significant portion of universities to use Java as their first programming language. It's not a hurdle for beginners. It's not a hurdle for experience programmers. If anything, it's a turn-off for petty-minded, superficial nitpicks. Among them are the kind of trolls who run around the JS community, bullying everyone who does not comply to their narrow vision of things. We'll do just fine without them.

Secondly: Haxe has always erred on the side of explicitness. If that is to change, then great (I guess). But there still should be a coherent way to drive language design forward (like if we can omit the class, can we also omit the package statement, which is fully redundant?), rather than uncoordinatedly cramming in party tricks here and there. The premise is fine, but the proposed solution falls short and may well prove an obstacle in adopting a design that addresses the problem.

Lastly: the Spoon language has all kinds of neat syntactic toys. Project got abandoned because nobody really cares at the end of the day. The same fate is dawning upon CoffeeScript. I am all for streamlining our syntax to our usecases. But I do fail to see the use case here. Some indentation is not that big a deal. As pointed out, Scala has the same restriction and while I have never claimed it to be representative of anything, it still ranks as the most proliferated FP language on GitHub. In light of that, the claim that having to nest your code one extra level (as for example in AS3, where everything is in a package statement for no good reason) is a critical impediment to adoption falls flat.

Pauan commented 7 years ago

@back2dos

This proposal will not, in the slightest, allow to write class-less scripts in Haxe.

You can write class-less scripts if you define a top-level main function.

But to think that trivial changes like these will bring us even anywhere near Smalltalk, Self, Lisp (with Light Table while you're at it), Apparatus or the gazillion of other well engineered interactive environments is outright stupid and an insult to decades of creative research into the field.

Nobody claimed that this change by itself will lead to massive adoption.

Nobody claimed that this change by itself will suddenly make Haxe an excellent teaching language.

You are the only one claiming that.

We have only claimed that this change moves things in the right direction. It nudges things closer to where they should be.

If your criteria for judging proposals is "it has to fix all of the problems all at once (including unrelated problems)", then nothing will ever get fixed.

Improvements have to be made one step at a time. This proposal is one of those steps. Nobody here believes that this is the only step, and nobody here believes that it is a big step. But it is a step.

A REPL would maybe be a start to get anywhere near that.

That's a great idea! And it's also completely unrelated to this proposal. You are free to make your own proposals if you wish.

Secondly: Haxe has always erred on the side of explicitness.

All of the top-level vars and functions are fully explicit. The only implicit part is the creation of the class.

Haxe already implicitly lets you leave off the class name if the class name is the same as the module name, so this proposal is consistent with Haxe's module system.

like if we can omit the class, can we also omit the package statement, which is fully redundant?

No you cannot omit the package keyword, because that actually conveys necessary information. How would you propose that we remove it?

The static keyword on the other hand does not convey necessary information, it's just extra noise.

It's not a hurdle for beginners. It's not a hurdle for experience programmers.

It is a hurdle for some beginners, it is a hurdle for some experienced programmers.

If that was not true, then why do you think the Try Haxe site allows for top-level vars, functions, and expressions?

The fact that Try Haxe is inconsistent with the rest of the language causes problems, and you claim that there is no benefit to top-level vars and functions, so why do you think the creators of Try Haxe chose to allow top-level vars and functions despite the problems?

rather than uncoordinatedly cramming in party tricks here and there

I'm not sure why you're claiming this is "uncoordinated": this is a valid proposal which follows all of the rules and protocol of haxe-evolution.

It doesn't suddenly open the flood-gates to millions of other syntax proposals, because every proposal is judged independently on its own merits.

And the core Haxe team will of course think carefully about this proposal before voting on it. There's absolutely nothing "uncoordinated" about it.

proposed solution falls short and may well prove an obstacle in adopting a design that addresses the problem

I do not see how this proposal creates an obstacle for anything, could you clarify?

And what alternate proposal would you have for people who like having top-level vars and functions (which is the vast majority of programmers)?

The same fate is dawning upon CoffeeScript.

You pointed to failed languages to try to prove your point, but you ignore all of the successful languages.

CoffeeScript failed because ES6 became popular, and one of the reasons ES6 became popular is because of having nice syntax sugar.

Every language has syntax sugar. The differences are in how much syntax sugar they have, and what kind of syntax sugar they have.

Claiming that allowing top-level vars and functions is "too much syntax sugar and will cause your language to fail" is absurd.

The vast overwhelming majority of successful languages have top-level vars and functions.

The vast overwhelming majority of programmers are used to top-level vars and functions.

This isn't a "JavaScript vs Haxe" thing, it's not a "functional programming vs Haxe" thing, it's a basic language thing.

Lastly: the Spoon language has all kinds of neat syntactic toys. Project got abandoned because nobody really cares at the end of the day.

The truth is that language popularity is really complicated, and it's often unclear why a language failed or succeeded.

You claimed that Spoon failed because nobody cares about syntax sugar, but you have presented no evidence for that claim. The burden of proof is on you to back up your claims.

As pointed out, Scala has the same restriction

Scala caters to Java programmers who are already used to using classes for everything. Scala is popular because Java is already popular.

I very seriously doubt that Scala is appealing to non-Java programmers.

In light of that, the claim that having to nest your code one extra level (as for example in AS3, where everything is in a package statement for no good reason) is a critical impediment to adoption falls flat.

Nobody claimed it is a critical impediment, just that it's an impediment.

Some people will consider it a major impediment, other people (such as yourself) will consider it not an impediment at all. People have different preferences.

If your goal is to appeal to Java programmers, then the mandatory class isn't an impediment at all.

If your goal is to appeal to web developers and functional programmers, then yes it is a rather significant impediment, because those types of programmers are used to having top-level vars and functions.

If you don't believe me that it is an impediment, go talk to some web developers and non-Scala functional programmers, show them some Haxe code and tell them that the class is mandatory, and that all functions and vars must have the static keyword. See what their reaction is.

But of course when you see their reaction, you'll just dismiss them as "petty-minded superficial nitpick trolls who should be dismissed because they will never contribute to Haxe". Despite the fact that they do contribute to the languages that they are currently using.

Imagine that Haxe 5 was released, and one of the syntax changes is that now all methods must have a new method keyword:

class Foo {
  public method function foo(): Int {
    return 5;
  }
}

This new method keyword is mandatory on all methods, and it serves no useful purpose.

Would you accept that? Would you think that is okay?

And if some existing Haxe users said "screw that, we're never going to upgrade to Haxe 5, we're staying with Haxe 4 forever", would you call them "petty-minded superficial nitpick trolls who will never contribute anything to Haxe"?

From the perspective of non-Haxe users, that's exactly the situation they're in: they are used to having top-level functions and vars, and now you are telling them that they must add in this useless static keyword to every var and function.

it's a turn-off for petty-minded, superficial nitpicks. Among them are the kind of trolls who run around the JS community, bullying everyone who does not comply to their narrow vision of things. We'll do just fine without them.

Please stop claiming that the vast majority of programmers should "be dismissed because they will not contribute to Haxe" and are "petty-minded superficial nitpick trolls".

It's completely fine if you don't like the proposal.

It's completely fine if you think the proposal is useless.

It's completely fine if you disagree with the proposal.

But please stop insulting everybody who disagrees with you.

Not everybody who has different preferences than you is a moron or a troll.

A large part of language design (especially syntax) is subjective and people will disagree.

That's okay, not everybody can agree on everything, but that also means that other people aren't wrong just because they disagree with you.

Pauan commented 7 years ago

I would like to make something very clear:

Mandatory classes are an impediment for some people. Therefore this proposal will increase adoption rates.

But we don't know how much: will it increase adoption by 0.1%? 1%? 5%? 10%? 50%?

We have not done any interviews, any polls, any studies, therefore the only correct answer is "we don't know"

Any talk about adoption rates is just a guess, so I won't be arguing about it anymore.

In addition, this proposal does not mention adoption rates.

Therefore, this proposal should be judged based on its own merits:

nadako commented 7 years ago

@Pauan again said everything I had to reply in a more comprehensive way than I possibly could. :)

I can only add that I indeed don't see much of a point in package statements, since packages always strictly follow directory structure and they didn't ever do anything for me other than being nuisance when moving modules around. But that's a completely different topic for another evolution proposal.

Anyway, I made some edits to the proposal, answering some questions and going with a simple primary-module-class approach.

Pauan commented 7 years ago

@nadako It seems a little inconsistent to me that module-level functions and vars are automatically public

I wouldn't mind having them be private by default, but I don't have a strong opinion either way.


Auto-importing the functions and vars is also inconsistent.

How do you propose that Haxe programmers should fix name collisions if two different modules create top-level functions/vars with the same names?

nadako commented 7 years ago

I also added more to-the-point code sample that illustrates that module-level functions works with other type declarations.

nadako commented 7 years ago

@Pauan

It seems a little inconsistent to me that module-level functions and vars are automatically public

You might be right about this, but I also don't have a strong opinion here.

Auto-importing the functions and vars is also inconsistent.

I disagree, because when you import a module, you import all its declaration into your module namespace, so if we have module-level function declarations it's only consistent to import them as well.

How do you propose that Haxe programmers should fix name collisions if two different modules create top-level functions/vars with the same names?

Like they do that now, by reordering imports or writing more specific import (import MyModule.concreteMethod, import MyModule.concreteMethod as differentMethod, etc.)

nadako commented 7 years ago

You might be right about this, but I also don't have a strong opinion here.

On the second thought, I do: normally, types declared in a module are public unless explicitly private, so this should apply to module-level functions and vars too.

Pauan commented 7 years ago

@nadako

I disagree, because when you import a module, you import all its declaration into your module namespace, so if we have module-level function declarations it's only consistent to import them as well.

On the second thought - I have: normally, types declared in module are public unless explicitly private, so this should apply to module-level functions and vars too.

You're right, but at the same time static fields are automatically private, and top-level functions and vars are indeed static fields, so it's a bit odd.

After thinking about it more, I suppose from the programmer's perspective top-level functions/vars are more similar to top-level enums/classes than static fields, so having auto-public and auto-import is fine.

Like they do that now, by reordering imports or writing more specific import (import MyModule.concreteMethod, import MyModule.concreteMethod as differentMethod, etc.)

Okay, great, I don't have any more concerns with the proposal.

nadako commented 7 years ago

from the programmer's perspective top-level functions/vars are more similar to top-level enums/classes than static fields

Yes, the fact they become static fields is an implementation detail.

Pauan commented 7 years ago

@nadako You might want to put an explanation for why they are auto-public and auto-import, since the reader's first instinct is to think that it's inconsistent with static fields.


Also, you mentioned that the code generator can optimize it by not creating the implicit class. You also mentioned that optimization won't work with reflection.

However, it is possible for the code generator to hoist all static fields (vars and functions) to the top-level.

So for example, this Haxe code:

class Foo {
  public static function foo(): Int {
    return bar();
  }

  public static function bar(): Int {
    return 5;
  }
}

Would get compiled into this JavaScript code:

function foo() {
  return bar();
}

function bar() {
  return 5;
}

function Foo() {}
Foo.foo = foo;
Foo.bar = bar;

Because the fields are hoisted, it can dispatch to them directly (it calls bar(), not Foo.bar()), but reflection still works because the fields exist on Foo

And if reflection isn't used, the dead code eliminator can then remove the foo and bar fields (and also remove the Foo class if it doesn't have any fields left).

So in the common case it would only output the foo and bar functions, and would not output the Foo class.

This optimization should be possible for all static fields for every class, so it would automatically apply to top-level vars and functions as well. No special rules needed!

nadako commented 7 years ago

Good catch @Pauan, thanks!

Pauan commented 7 years ago

I thought about it some more, and my optimization idea is trickier than I thought: it could still cause things to break if Reflect.deleteField, Reflect.setField, or Reflect.setProperty are used.

However, it's possible to fix the breakage by using ES5 getters/setters.

But maybe we don't need to fix it, because the Reflect API makes no guarantees about correctness: it even specifically says that the Reflect API might not work because of dead code elimination.

In any case, this is starting to get off-topic, so this optimization idea should be moved to another issue.

My point was just that we can optimize all static fields of all classes, so we don't need special optimization rules for top-level vars/functions.

nadako commented 7 years ago

You might want to put an explanation for why they are auto-public and auto-import, since the reader's first instinct is to think that it's inconsistent with static fields.

Done.

fullofcaffeine commented 7 years ago

Sooooo, should we merge this now? 🙄

nadako commented 7 years ago

Sooooo, should we merge this now? 🙄

Well unfortunately none of the core developers expressed their opinions yet. Also after looking into implementing it a bit, I'm considering lifting the "primary module class" restriction, because I now think supporting both primary module type and module-level functions/vars wouldn't be THAT hard.

fullofcaffeine commented 7 years ago

Can we ping them? I feel that evolution docs, even after ready, take a lot of time to be approved/merged because of that. A good habit might be to always ping all the core team members here when it's ready for review.

nadako commented 7 years ago

Well, sure: @ncannasse @Simn @hughsando @jdonaldson @andyli @waneck

waneck commented 7 years ago

I really like this

jdonaldson commented 7 years ago

I'd be willing to do the work to support this for lua. But, the way I see it I shouldn't have to do anything at all... so maybe I'm not getting the whole picture.

Is it really necessary to define new AST nodes for this? Why not just infer the class name from the file name, generate a wrapper class in the AST for it, and add special metadata to it to indicate that it behaves as a module level class. Even for targets that have no notion of class, it will still be important to namespace the code so that methods and variables do not collide. "Pure" module level functions won't really generate faster code either... most targets sensitive to this have flattened namespaces.

Also, can we reliably differentiate between class-level and field-level metadata for this? Some metadata applies to both.

As a side note, it would be nice to handle non-oop class constructs with this (e.g. abstract/extern)

jdonaldson commented 7 years ago

Also, kudos to @nadako for all the documentation work on the proposal. Very succinct and clear.

nadako commented 7 years ago

Is it really necessary to define new AST nodes for this? Why not just infer the class name from the file name, generate a wrapper class in the AST for it, and add special metadata to it to indicate that it behaves as a module level class. Even for targets that have no notion of class, it will still be important to namespace the code so that methods and variables do not collide.

What I'm proposing is to have a separate TypeDefKind for module-levels in the untyped AST (stuff represented by haxe.macro.Expr structures), because untyped AST basically represents syntax and there's no class in the syntax. While when it's typed into the typed AST (stuff represented by haxe.macro.Type structures), I indeed propose to create a wrapper class. Then a generator can choose to omit the wrapping class or not.

Also, can we reliably differentiate between class-level and field-level metadata for this? Some metadata applies to both.

Well since there's no class for module-levels in the syntax, one can't add class-level metadata for them. I think that's a non-issue, because it's rarely needed and it's still possible to create a normal class with static methods and metadata.

As a side note, it would be nice to handle non-oop class constructs with this (e.g. abstract/extern)

Not sure I understand what you mean...

hughsando commented 7 years ago

I love the idea of a top-level main function/other functions. The details I will leave up to you.

andyli commented 7 years ago

I think the title and focus should be "module-level properties (i.e. with get/setter) and methods" instead of "module-level variables and functions".

The proposal mentioned Properties can be supported too., but really, I think it should be supported.

In Haxe, currently,

As you may have noticed, the module level is more similar to the class level than the expression level. That means promoting things from the class level (instead of the expression level) to the module level can be more appropriate. And I can see that this proposal is already talking about the class level var and function instead of the expressions. I see no obvious reason to leave properties behind.