HaxeFoundation / haxe-evolution

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

Constructor `this.arg` syntax #97

Open RblSb opened 2 years ago

RblSb commented 2 years ago
class People
  final name:String;
  final age:Int;
  function new(this.name, this.age) {}
}

Rendered version

nadako commented 2 years ago

I like it and I think we actually discussed this idea on the dev Slack before, but I struggle to remember if we had any unclarities or something against it...

nadako commented 2 years ago

I just found a discussion that this actually comes from Dart language, and also a link to a macro I wrote to implement similar thing: https://gist.github.com/nadako/273497023683e20f964ccd6ac0643422

Aurel300 commented 2 years ago

How do we express this in the AST? An extra (nullable) flag on haxe.macro.FunctionArg that can only be used on constructors?

back2dos commented 2 years ago

There's still a bit of redundancy, that I would quite like getting rid of.

Consider a somewhat complex class, with members grouped by concerns:

class Airplane {
  var captain:Pilot;
  var copilot:Pilot;
  // ... and more crew related stuff
  var thrust = .0;
  var pitch = .0;
  var roll = .0;
  var yaw = .0;
  var lat:Float;
  var lang:Float;
  // ... a dozen of functions to control the aircraft's rotation/position/acceleration
  // ... radio
  // ... baggage
  // ... and so on ...
}

But there is only one constructor. So one has to go back and forth between that and declarations to see which ones are initialized in it.

In tinklang you can for example write `var lat:Float = ;. I guess the concrete syntax is a matter of taste, but it avoids spreading things out. It could benew var lat:Float;` or something.

RblSb commented 2 years ago

I like new final foo:Float, this is less flexible, so you cannot change order of fields independent of argument order, but at the other side arguments order is easier to follow. But i think this needs something in constructor, to get attention for existed new fields, like: function new(this.args, bar:String) {} So you also can add non-field args before or after.

This also opens question, if we should assign null from argument to field with default value: new final count = 0;

back2dos commented 2 years ago

Hmmm. Valid points.

In the spirit of assigning arbitrary meanings to _ tinklang allows also using it to specify where auto generated constructor args go, so in `function new(foo:String, , bar:String)` they would go in the middle (there's no finer grained control).

This also opens question, if we should assign null from argument to field with default value: new final count = 0;

Yeah, that's a good point. That should probably make 0 the default value (otherwise it's meaningless).

In the end short hands and complex classes as the example I gave before are perhaps not the best fit, so maybe I'm pushing this in a somewhat useless direction ;)

nanjizal commented 2 years ago

@:structInit is awkward to remember hard to google and verbose, and where I would like short cut most, I am still seeing repetition, it could be simpler.

class People_ {
    public function new( public final this.name: String, private var this.age: Int ){}
}
abstract People( People_ ){
   public inline function child(){
       return this.age < 16;
   }
}
function main(){
    var boy: People = { name: Oliver, age: 12 };
    if( boy.child() == true ) trace( '$(boy.name) is child' );
}
nanjizal commented 2 years ago

oops forgot the 'new'.. but may have over simplified from real haxe anyway. You probably don't need 'this'... with my thought.. This should allow typedef construction by default as option.

class People_ {
    public function new( final name: String, public var age: Int ){}
}
nanjizal commented 2 years ago

for lots of properties splitting by line would work fine.

class People_ {
    public function new( 
      final name: String
    , public var age: Int ){}
}
TheDrawingCoder-Gamer commented 2 years ago

Kotlin allows the paramaters to be put in the class header

 EnergyStoragePlugin(private val energyStorage: EnergyStorage)
rhysuki commented 2 years ago

@nanjizal I'm not a fan of this. it trades clarity for brevity, adding a totally different spot for something as fundamental as field declaration to go into. it could introduce sneaky fields if you ever want to mixmatch between regular declaration and in-constructor declaration:

class Person {
  public var name: String;
  public var age: Int;
  public var friends: List<Person>;

  public function new(this.name, age: Int, ?friends: List<Person>, var id: Int) {
    this.age = age;
    this.friends = friends ?? new List<Person>();
  }
}
player-03 commented 1 year ago

But there is only one constructor. So one has to go back and forth between that and declarations to see which ones are initialized in it.

The thing is, that's true with or without the new syntax.

class Airplane {
  var captain:Pilot;
  var copilot:Pilot;
  // ... and so on ...
  public function new(captain:Pilot) {
    this.captain = captain;
  }
}

captain is initialized, copilot isn't, and you have to scroll down to find that out. But there's also already a couple solutions available:

class Airplane {
  final captain:Pilot;
  var copilot:Null<Pilot>;
  // ... and so on ...
}

Now you know that captain must be set, and you should expect copilot to be null sometimes.

Alternatively, just write comments. I'd argue that if you're writing a class that's big enough for this to be a problem, you can take responsibility for code clarity. Haxe doesn't need to force it, at least not at the expense of constructor usability. (More on that later.)

I'm not a fan of this. it trades clarity for brevity, adding a totally different spot for something as fundamental as field declaration to go into.

Agreed. If I was searching for a certain field, I would definitely not think to check the constructor.

I do want shorter code, but not at the expense of clarity.


So at this point I've spoken out both for and against clarity. Evidently this isn't a simple issue. Let's talk about that.

I see this as a tug-of-war between brevity and clarity/usability.


Aside: code completion (and related tools) can improve clarity/usability. However, it isn't always available, so I'd argue we shouldn't rely too heavily on it.

player-03 commented 1 year ago

My opinion: I prefer RblSb's original proposal over the others, but I find it hard to read and I'm not ready to endorse it.

I feel like I'd have trouble justifying any of the proposals to a newbie Haxe programmer. They're all hard to decipher at first glance. Why are some of these arguments prefixed with this. and then never referenced? Why do some of the arguments say var and final, and are also never referenced? What are all these underscores?

Given that we already have @:structInit, I'm starting to think that's all we need. At the very least, it's easier to Google than any of these other options.

Edit: I take that back. If @:structInit generates a constructor, the constructor won't be inline. That's kind of useless then.