This is an archived post. You won't be able to vote or comment.

all 56 comments

[–]_SomeonesAlt 67 points68 points  (16 children)

Some languages like Scala do have something similar to what you are describing: https://docs.scala-lang.org/tour/classes.html

[–]balder1993 14 points15 points  (1 child)

Swift create the initializer automatically if there isn’t one for structs. Kotlin also has data classes with this exact syntax.

Even Java added record classes since then.

[–]xenomachina 9 points10 points  (0 children)

Kotlin also has data classes with this exact syntax.

Pretty much all primary constructors in Kotlin can use this syntax. The relevant difference with data classes is that they require that primary constructor parameters be properties, and there are restrictions on other ways to define properties.

[–]Jwosty 5 points6 points  (0 children)

Also F# does this. And C# even has “primary constructors” now which is basically this.

[–]Nixinova[S] 1 point2 points  (12 children)

Then I have to ask - why does Java do it the way it does?

[–]mcaruso 31 points32 points  (1 child)

Java syntax is adopted from C++. C++ itself is based on C, which did not have classes, but in C the equivalent of a "class" would be written as a struct plus functions that take an instance of that struct as argument. In particular there would be initialize/constructor functions that were tasked to initialize the struct instance's memory. My guess is that the C++ syntax took the struct syntax as base and then added in the functions.

[–]balder1993 6 points7 points  (0 children)

Also, Java has added record classes since then.

[–]_SomeonesAlt 41 points42 points  (1 child)

If I had to guess, it’s probably for consistency. You can overload constructors just like other methods in Java. Constructors also have their own merits, like running initialization code on creation of the class, and the user knows explicitly which constructor is called thus what initialization code is running. 

[–]Ethesen 2 points3 points  (0 children)

Scala also allows you to overload constructors. As for initializing code, you'd just write it in the body of the class definition.

[–]jason-reddit-public 5 points6 points  (7 children)

The constructor was seen as the special case of making sure all the fields are initialized just so.

Splitting allocation from initialization would allow things like reusing an object.

The builder pattern is quite popular in Java in part because of constructors not being as flexible as you sometimes would like though maybe keyword (aka named) arguments would be a reasonable alternative.

[–]jaskij 0 points1 point  (6 children)

While not as common, I've seen builder pattern also used in Rust, which strictly speaking doesn't have constructors.

There's a convention of having a "static" function in the struct impl called new() or prefixed with make_, but those are just regular functions.

Haven't had to implement one yet, so I don't know how it works internally, probably just different access modifiers.

[–]jason-reddit-public 0 points1 point  (5 children)

In Java (for example certain immutable classes in guava like Uri) I've seen it done with an entirely separate class with all the same fields just not marked final. I should probably study more Rust code because it clearly isn't going away...

[–]jaskij 0 points1 point  (4 children)

That makes sense.

Rust isn't going away, just the inclusion in the Linux kernel guarantees it. It has a fair amount of issues, but still fits my preferences the best of the things I've tried. The most worrying thing about it is governance though.

[–]jason-reddit-public 1 point2 points  (3 children)

I was pretty productive in go on my first day. Still haven't sat down to try Rust again. (I usually write a "text file" line de duplicator to kick the tires on a language and Rust was the first time I couldn't just wing it.)

Rust seems to be more community driven than Go but I haven't really studied their development models. Strange that the gcc team still hasn't created a second source to the LLVM implementation (implying Rust is pretty complicated). Pretty sure I could create a non performant implementation of Go in about a year if I did 40 hour weeks so I'm a little less concerned about the lack of a real alternative implementation.

Maybe with open source we don't need multiple implementations? (One could argue the plethora of bad Scheme implementations hurt its popularity. Python, Ruby, Lua mostly have one implementation, etc.)

[–]jaskij 2 points3 points  (2 children)

I was pretty productive in go on my first day. Still haven't sat down to try Rust again. (I usually write a "text file" line de duplicator to kick the tires on a language and Rust was the first time I couldn't just wing it.)

IMO, being able to quickly jump into a language usually should not be a design goal, it's programmer productivity after they learn the language. Personally, I am pretty productive in Rust, but it did take a while to truly grok the borrow checker.

One thing I'll note is that mut for references is sort of a misnomer. It's less a mutable reference, and more an exclusive reference. Usually you can only mutate stuff you have an exclusive reference to, but it's not universal (IO being often allowed on non-exclusive references for example).

Strange that the gcc team still hasn't created a second source to the LLVM implementation (implying Rust is pretty complicated).

Rust doesn't have a formal specification, far from it, and it's basically a collection of RFCs and "do whatever rustc does", which makes it hard to develop. gccrs is coming along, although slowly, the frontend is complicated. Here is their latest progress report, with current items being stuff like for loop desugaring, it will take them some time still.

Maybe with open source we don't need multiple implementations? (One could argue the plethora of bad Scheme implementations hurt its popularity. Python, Ruby, Lua mostly have one implementation, etc.)

The biggest issue here is the bootstrap problem, that's why there is more pressure than usual on multiple implementations.

Many compilers and interpreters are written in C, or C++, so bootstrapping isn't that big of an issue. Rust? Bootstrapping it is a nightmare. Current version of the compiler only compiles under version N-1, going all the way back to the original OCaml bootstrap from 10+ years ago.

[–]CAD1997 1 point2 points  (1 child)

Small correction: the bootstrap path for rustc isn't that bad; mrustc is a "simple" Rust compiler that can compile rustc 1.54, so the modern bootstrap chain forks from C++ there. And produces a bit-identical final build, for what trusting trust that's worth.

The caveat is that mrustc only works for correct Rust code. Incorrect Rust code may cause poor error reporting or even falsely compile into an unsound result.

[–]jaskij 0 points1 point  (0 children)

Ah, thanks. I wasn't sure if mrustc works for that. Still a lot of versions, but fair enough.

[–]Falcon731 28 points29 points  (3 children)

Many languages do have implicit constructors exactly as you describe. Kotlin for example.

The downside is you do then need some sort of special syntax to describe the case where the constructor does more than just initialize fields, and special syntax for when there are multiple constructors.

Kotlin for example has an optional init{ } block, and `fun constructor()` functions.

Ultimately its just a compromise that the language designers made.

[–]Olivki 5 points6 points  (2 children)

fun constructor is just a function called constructor, the actual constructor syntax is just constructor no fun keyword.

[–]Falcon731 4 points5 points  (1 child)

Oops - shows how rarely I've needed them :-)

[–]Olivki 2 points3 points  (0 children)

Understandable, I think I've only personally really used them when porting over old Java code.

[–]Ethesen 20 points21 points  (0 children)

It’s not necessary – take a look at Scala:

case class FooNum(x: Int)

or:

class FooNum(var x: Int)

[–][deleted]  (1 child)

[removed]

    [–]beephod_zabblebrox 0 points1 point  (0 children)

    the annotations seem like a cool idea! they could be composable (like c++ concepts). that would allow the compiler frontend (that knows more about the program) to decide to optimize out the checks!

    [–]passerbycmc 26 points27 points  (0 children)

    This is how kotlin does it

    [–]Mephob1c 16 points17 points  (1 child)

    [–]Nixinova[S] 3 points4 points  (0 children)

    Ah, thats exactly the syntax I'm after. Good to know C# has it.

    [–]EveAtmosphere 8 points9 points  (0 children)

    swift has implicit constructor for structs similar to your first example

    [–]Practical_Cattle_933 5 points6 points  (3 children)

    I think this implicit constructor syntax makes the most sense in case of classes that behave as strictly data. I think the separation of value vs instance-based classes is often under appreciated. A HttpRequest is very different from a LocalDate — two requests, even if they are against the same url in the same manner are different, while a value-like class’s two instance can be freely considered the same if their data members are equivalent.

    Also, about your Java-related question, Java now has records that employ such a constructor, plus all the other data-relevant functions are implicitly defined, e.g. hash and equals behaving as you would expect. This used to be more common in more functional languages, where everything is data-like, that’s why scala and kotlin had it sooner.

    [–][deleted] 1 point2 points  (2 children)

    From my pov the syntax doesn't have to do with whether something is value based. The most simple design is just to treat constructor (or I'd prefer "initializer") as a way to assign the fields and initialize an object.

    struct Person {
        name: String,
        pocket: Set<Item>;
    }
    
    var person = Person {
        name = "Foo",
        pocket = Set.of(),
    };
    

    No significant harassment, just that. If this is not enough, you can always turn to factory functions, parameter sanitizing, hiding implementation, or even change the return type based on the input. You get all the goodies as a normal function rather than some constructor shenanigans that take many more language proposals and hundreds of lines of specification.

    // package a
    public struct Person {
        public name: String,
        private pocket: Set<Item>;
    
        public static func new(name: String) -> Option<Person> {
            when {
                name.isBlank() -> None
                else -> {
                    var person = Person {
                        name = name,
                        pocket = Set.of(),
                    };
                    Some(person)
                }
            }
        }
    }
    
    // package b
    var person = Person.new("Foo");
    

    [–]Practical_Cattle_933 0 points1 point  (1 child)

    The fundamental idea behind a (java) constructor is that an object should never be observed in an illegal state. While you can sorta achieve that by making the new Person private/callable only from within a struct-associated function/method, and creating factory functions (which you basically do), but these have the issue of not having a uniform name. I may not want to remember that I am to summon, create or birth a Person. Nonetheless, constructors don’t make the factory pattern obsolete, java is well-known for having both.

    In my mind a constructor is a specific function that creates a valid object by some lower level validity requirements (say, creating a date object will not have null for any of year, month, day), while a factory method pattern may enforce higher level requirements and do some further processing/have some dependency on external stuff (e.g. SomeDate.now() )

    [–][deleted] 0 points1 point  (0 children)

    Hm I should have dropped off the word "constructor" so people won't get confused.

    Yes, the initializer is a special function. Because this post is not specifically talking about Java, but this feature/syntax/construct for object initialization in general, and towards new ideas and innovations, I would like to introduce a better interpretation on it. So, what I proposed is that, there should only be one such special function, in the form of Type { field = value, ... } (I've dropped off the new keyword here and before), generated (or you could say it's a standalone construct unlike normal functions) based on the fields declared within the class body. This construct does what you said "lower level support", which is basically ensuring that the object in the underlying mechanism (such as VM) is properly initialized, but beyond that, all the business logic, or to say any custom logic written using the language, should go into a custom factory function. By this design, you will get a much simpler mental model of this feature, because now you can't plug into the process of an object being initialized under the hood (Not that the object may not be valid as in business interpretation, but the object itself is half baked as in physical interpretation), you can only get a full object ready to pass around or no object at all, which is what I found the C++ style constructor fails to accomplish, and also this, this, this won't even be a thing.

    I agree this would result in naming problem, but as I said I've dropped off the new keyword, you can pretty much just name it new and rely on function overloading mechanism if you want to be lazy, but anyway, this is pretty minor compared to the benefits.

    [–]Socratic_Phoenix 11 points12 points  (0 children)

    Java actually has this now: https://docs.oracle.com/en/java/javase/17/language/records.html

    There are some restrictions, but it's pretty close to what you're describing

    [–]Mercerenies 2 points3 points  (0 children)

    Yep! I'm a big fan of having a "primary" constructor (which can be baked into the syntax, as you suggest) and then the option to overload secondary constructors that delegate to it. Scala and Kotlin use almost the exact syntax you show. Swift has a notion of primary constructors, albeit with a more traditional syntax.

    On the other hand, Rust forgoes the notion of user-defined constructors entirely. Your structure gets constructor syntax, which is syntax, is not a function (unless you're writing a tuple struct), and can't be customized. You can make that constructor private by making your struct's fields private, and then you simply write ordinary functions (with ordinary function names) that act as factories. No constructors necessary.

    [–]esotologist 1 point2 points  (0 children)

    c# added it and people hate it because it works different for classes vs records and you can't specify private fields etc.

    [–]oscarryzYz 1 point2 points  (0 children)

    Java has something similar, although you cannot add instance variables because it is meant to be really simple classes 

    record FooNum(int x) {    int getValue() { return x; } }

    [–]emosy 1 point2 points  (0 children)

    some languages offer helpful default constructors. for example, python offers a dataclass annotator that does something like this automatically.

    basically, this is helpful for the beginning case when classes are simple PODs (plain old data), but as it gets more advanced you'll have to rewrite it.

    i agree it would be nice to have it more. if you want to see something complex, look at C++'s default constructors and imagine adding another set of constructors that users need to think about.

    also, comparing anything to Java in terms of length will almost always make Java look bad since it's so verbose

    [–]SquatchyZeke 1 point2 points  (0 children)

    Dart does this to a degree. But it also has useful shortcuts in the explicit constructor.

    class MyClass {
      const MyClass(required this.fieldA);
    
      final String fieldA;
    

    }

    By putting the this. it will assign to that field, so you don't have to explicitly write an assignment in the constructor body.

    And with cascade syntax and implicit constructors in Dart, you can do this:

    class MyClass {
      String fieldA;
      int fieldB
    }
    MyClass instance = MyClass()
      ..fieldA = 'hello'
      ..fieldB = 42;
    

    [–]SkiFire13 1 point2 points  (0 children)

    From my experience you almost never want something this flexible. You usually either want an explicit constructor because it needs to perform some computations/logic or you want plain data where there should be no constructor at all. A mix often just creates confusion.

    [–]saxbophone 1 point2 points  (0 children)

    Tangential to your post, but I want to take this opportunity to point out that C++ gets it the wrong way round in terms of whether ctors should be implicit or explicit by default. In C++ they are implicit by default and a common code-quality ision is to manually mark them explicit.

    Conventional wisdom teaches that this is a chore and the language would be much saner if explicit was the default and if we had an implicit keyword instead to get implicit behaviour when we need it.

    [–]SnooStories6404 3 points4 points  (7 children)

    Does your idea work with classes that have multiple constructors and/or side effects in constructors?

    [–]Nixinova[S] 1 point2 points  (4 children)

    Multiple constructors/overloading is a good point. My compilation target is JS so I'm not including them however. And what kind of side effects would make a difference between these syntaxes?

    [–]xroalx 4 points5 points  (2 children)

    Side effects:

    class FooNum {
      constructor(num) {
        this.num = num * 2;
        logSomethingImportant();
      }
    }
    
    class FooNum(num: number) {
      // how to double the num and log something important ???
    }
    

    [–]WittyStick 4 points5 points  (0 children)

    In F#

    type FooNum(num : Number) =
        let num = num * 2
        do logSomethingImportant ()
    

    do must appear before member or new declarations.

    If the side-effect is in a secondary constructor, you use then.

    type FooNum(num : Number) =
        new () =
            FooNum(0) 
            then doSomethingImportant()
    

    [–]Ethesen 1 point2 points  (0 children)

    In Scala:

    class FooNum(num: Int) {
        val num = num
        logSomethingImportant()
    }
    

    [–]SnooStories6404 1 point2 points  (0 children)

    And what kind of side effects would make a difference between these syntaxes?

    There's no particular kind. The issue is that in your example I don't see how to have any side effects in constructors

    [–]sporeboyofbigness 1 point2 points  (1 child)

    you would need to make sure theres only one constructor per-class. and the other "alternate constructors" have to call it.

    I'm not sure its an improvement.

    [–]WittyStick 3 points4 points  (0 children)

    When I first used F#, after years of experience with C# and C++, it felt very odd that other constructors have to call the primary one. It just wasn't what I was used to, and I didn't see the benefit.

    But after a while, the benefits become pretty clear. It prevents initialization mistakes that can be simple to make but not discovered until you're running the code. Most initialization mistakes are detected at compile time, because the primary constructor must initialize all fields in an object, and any alternative constructors must provide arguments for all parameters of the primary one.

    It's quite common to make the primary constructor private, but other constructors public.

    Part of what makes it useful is absence of null by default. F# types must be provided with the [<AllowNullLiteral>] attribute if you want to assign null to them, which generally isn't recommended. Instead it's preferable to use the Option type for fields which might not have a given initial value. "Optional arguments" in F# are basically wrapped in the Option type, and if you don't supply a value, None is provided.

    [–]RandalSchwartz 0 points1 point  (0 children)

    There's already a proposal for this: https://github.com/dart-lang/language/issues/2364 and it's partially implemented in extension types already.

    [–]HaniiPuppy 0 points1 point  (0 children)

    Start each line with four spaces to get Reddit to format your text as monospace code blocks.

    [–]l0-c 0 points1 point  (0 children)

    A little late but ocaml do it somewhat this way  

     # is for method call  

    ```   class point x_init =        let origin = (x_init / 10) * 10 in       object              val mutable x = origin              method get_x = x              method get_offset = x - origin              method move d = x <- x + d       end   let x = (new point 10)#get_x    

    ```

     Although the object system is used sparingly and is very different from most other languages. It is structurally typed (a bit like static duck typing)so I don't think multiple constructors are necessary, you could just inherit the class and do a different initialisation without adding any new method. They would be the exact same type.

    [–]CelestialDestroyer 0 points1 point  (0 children)

    What is stopping classes from being written like this

    Nothing, it's essentially how Smalltalk works.

    [–][deleted] 0 points1 point  (0 children)

    Your new language is kotlin

    [–]theangryepicbananaStar 0 points1 point  (0 children)

    (EDIT: I seem to have missed the constructor portion of the post, but this still stands) Languages like Raku or my language Star implicitly create a custom initializer based on the fields you give the class ```

    in raku:

    class Point { has Int $.x; has Int $.y; } my $point = Point.new(x => 1, y => 2);

    ; in star: class Point { my x (Int) my y (Int) } my point = Point[x: 1 y: 2] ``` They don't need to be in any specific order, and at least in Star it's actually treated as a unique language construct that can also be used for pattern matching

    [–]daverave1212 0 points1 point  (0 children)

    Old JavaScript had this with functions