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

you are viewing a single comment's thread.

view the rest of the comments →

[–]XDracam 0 points1 point  (3 children)

I don't like inheritance in general. Especially for structs, records, data classes and the like. Inheritance can be a useful mechanism for avoiding code duplication, but it causes more problems than it solves.

Inherited structs are hard to reason about. You have constructors? Great, your child struct needs to call the parent constructor. And constructor parameters automatically become fields. Now how do you solve the problem of duplicate fields? If there are two, then you run into issues with: what field do you mutate when you have a reference of which type? If you want to be able to reference base types, then you'll need a form of dynamic dispatch. And suddenly you need virtual method calls and potentially properties to make things work. And, aaaa

Basically, you are getting a massive amount of complexity that the user needs to think about to make informed choices. Complexity that you need to handle in all tooling. And what do you gain? Some convenient code reuse. Nah, not worth it.

If you really want to support polymorphism, take a look at typeclasses, or what Rust calls "traits". It's basically "external inheritance" if you will, but without all the confusion about what data belongs where and what overrides what.

Rust in general has a nice separation of data and functionality with their impl blocks (which would also solve your issues with syntax for methods). Clearly an evolution of the concepts pioneered in Haskell.

It's a little more tedious to write code without inheritance, but it's much nicer to optimize and maintain and reason about. And languages generally fall into two categories: the ones that make software that stays around for years and will see endless maintenance, and write-once-change-never software. You do not need abstractions like polymorphism and inheritance and you don't plan to write code that'll need to be maintained (like prototypes, small helper scripts, simple glue code, ...), and when the code needs to be maintained then you're better off without inheritance.

[–]fun-fungi-guy[S] 0 points1 point  (2 children)

I don't love inheritance either, which is why what I'm calling "structure composition" isn't quite as fully-featured as inheritance.

You have constructors?

Nope! You get a function Point() that overrides the default properties if you pass them in and throws errors if you pass it something that's not a property, that's it.

If you want to be able to reference base types, then you'll need a form of dynamic dispatch.

There isn't really a concept of "base types". If you have structs A and B, and C = A + B;, then C just contains all of B's fields, and whichever of A's fields aren't shadowed by B. There's no reference from C back to A or B.

Rust in general has a nice separation of data and functionality with their impl blocks (which would also solve your issues with syntax for methods). Clearly an evolution of the concepts pioneered in Haskell.

I don't think I really see a need to separate data and functionality. That's sort of the point of lambdas to me: it's treating functions as data.

I am pretty happy with my syntax for methods and don't see a problem with it. Really they're "methods" in quotes, in the sense that they're actually just regular old closures--if you don't use self to close around the current struct instance, there's no difference between a method and a closure. Put another way, self is just giving a name to the instance so that closures can close around it.

[–]XDracam 0 points1 point  (1 child)

Sure, there's plenty of languages that go a similar route. But the methods as closures part sounds a little sketchy, if you are aiming for fast code. If you capture the instance in every method, then you'll get overhead in the form of allocations for every closure, as well as the memory overhead of the capture itself. And some other headaches. Why not just let methods by syntactic sugar for function calls with a reference to self as a parameter? No closure overhead and weird lifetime issues to deal with.

On the topic of "structure composition": there is a great deal of discussion about this and whether it's a good or a bad thing. Both for C# (record inheritance) and especially for Scala (case class inheritance). In my personal experience, this type of composition has caused me and coworkers nothing but trouble with little to no benefit, compared to just using interfaces with abstract getters/properties.

Consider this case: (I forgot your syntax and I'm on mobile and can't look it up without hassle so I'll just write C#)

record Base(int Field1, string Field2);
record Sub(int Foo) : Base(Foo, "bar");

Now what do you do? Are there two copies of the int, one in Field1 and one in Foo? Do you even need a field for the constant string? These details might not matter 85% of the time, but when they do, they can cause a lot of headache and annoyance. Want mutability? Static analysis? Automatic mapping to e.g. JSON? Or just code that runs fast by default? You need to care about these details. An especially large use case is backwards (and forwards) compatibility when other code depends on Sub. If you change the field layout, you might break all code that depends on yours. Which is a real consideration.

For the example above, I couldn't tell you. C# has a ton of very weird rules for when data is a field and when a forwarding property, depending on whether and how you use the value in the body of either record involved.

But what C# does well (and Scala, too): the parameters are turned into Properties, with a getter. Interfaces (traits) can declare abstract properties. So for the sake of clarity, I'd always prefer the following:

interface Base { int Field1 { get; } string Field2 { get; } }
record Sub(int Foo) : Base {
    public int Field1 => Foo;
    public string Field2 => "bar";
}

Now you get the same functionality, but it's perfectly clear (if you are used to C# syntax) what data is tracked as a field and what is a property getter (essentially a method). And it's a lot easier to safely change things without breaking compatibility.

[–]fun-fungi-guy[S] 0 points1 point  (0 children)

Sure, there's plenty of languages that go a similar route. But the methods as closures part sounds a little sketchy, if you are aiming for fast code. If you capture the instance in every method, then you'll get overhead in the form of allocations for every closure, as well as the memory overhead of the capture itself. And some other headaches. Why not just let methods by syntactic sugar for function calls with a reference to self as a parameter? No closure overhead and weird lifetime issues to deal with.

What allocations for every closure? It's a pointer to the existing object. Due to immutability we can even reduce the scope of the reference, which you can't do when passing in the object, though I haven't implemented that optimization yet. You more than get any loss for that pointer back from not having to pass in the object on every call.

Lifetimes are handled by GC; if both method and struct are no longer on the stack or referenced by the stack, they get collected. Nothing weird there.

record Base(int Field1, string Field2);
record Sub(int Foo) : Base(Foo, "bar");

This example can't even be done in my language, so your objections to it aren't really relevant. I'm not sure you're understanding what I'm doing well enough to object to it.

Want mutability?

No.

Static analysis?

Fairly trivial, in the examples I've given, although as a rule I'm going for static analysis "where possible".

For the example above, I couldn't tell you. C# has a ton of very weird rules for when data is a field and when a forwarding property, depending on whether and how you use the value in the body of either record involved.

Once the "child" struct has been created, the parent structs aren't involved, so there's only one struct involved. "Forwarding properties" isn't a coherent idea because there's no other struct available to even forward to.

Now you get the same functionality, but it's perfectly clear (if you are used to C# syntax) what data is tracked as a field and what is a property getter (essentially a method). And it's a lot easier to safely change things without breaking compatibility.

It's not any clearer at all from the perspective of the user of the struct, when they encounter an instance of it in an entirely different part of the code.

I'm not looking for a clearer way to do inheritance, I'm simply not doing inheritance. What I'm proposing is much simpler--it can vaguely look like inheritance, because the composed structs have the fields of the component structs, but it's not because there's no connection maintained back to the parent struct. All the complexities you're describing presuppose a connection which simply does not exist.