Open RblSb opened 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...
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
How do we express this in the AST? An extra (nullable) flag on haxe.macro.FunctionArg
that can only be used on constructors?
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 be
new var lat:Float;` or something.
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;
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 ;)
@: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' );
}
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 ){}
}
for lots of properties splitting by line would work fine.
class People_ {
public function new(
final name: String
, public var age: Int ){}
}
Kotlin allows the paramaters to be put in the class header
EnergyStoragePlugin(private val energyStorage: EnergyStorage)
@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>();
}
}
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.
this.varName = varName
), while keeping the constructor arguments about the same (you have to add this.
but can remove the type hint).for all of them). You do have to mark the field declarations in some way (possibly with
= _or
new final`)._
.= _
or new final
, then you can instantly see which is which.function new(foo:String, _, bar:String)
means, and those who do will still have to scroll back up to the variable declarations to find which arguments to pass. (Hope they don't miss any!)function new(this.name, this.age)
doesn't tell you the variable types. name
is easy enough to guess, but for age
you'll have to scroll back up to check if it's an Int
or Float
. (Assuming for the sake of example that this is a large class.)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.
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 At the very least, it's easier to Google than any of these other options.@:structInit
, I'm starting to think that's all we need.
Edit: I take that back. If @:structInit
generates a constructor, the constructor won't be inline
. That's kind of useless then.
Rendered version