Hacker News new | ask | show | jobs
by nickm12 1653 days ago
https://lwn.net/Articles/548560/

I really enjoyed the article above, which I read many years ago (before Rust 1.0!) which discusses how Golang and Rust handle polymorphism and code-reuse without classic object inheritance. My current thinking is that software objects are a general-purpose tool, but classic object inheritance should rarely be used as it is a solution to a narrow problem—classes should be "final" by default, and if not the inheritance pattern should be completely designed up front.

Java had the misfortune to be designed at a time when OOP was the new craze and the design decision to force all code into an object hierarchy has not held up well. I'd rather use languages designed either before or after Java, where you can use objects when they are appropriate and ignore them when they aren't.

2 comments

I mostly agree, but there's one place where it does make a lot of sense to keep the hierarchy open: exceptions. Ability raise a specific error and catch it in a generic handler is very useful.
Interfaces would work fine here as well.
As someone who is greatly in favor of composition over inheritance, I don't agree - or at least my experiences don't point that way.

Both Rust and Go have had medium sized warts and/or boilerplate around errors, especially if you need control flow to depend on the error. In Python I've never felt that way.

Not sure I can put my finger on it, because any trivial example would be fine in either paradigm. I think it has to do with the forced/unnatural upfront decision "is this error a type or an interface" that may change later on, as a type might need to be refactored to an interface. There's probably one or two other reasons I can't think of right now.

Standard ML and OCaml have both sum types (like in Rust) and exceptions. I feel like both have their value, and both are important. A nice thing is that you can match on exceptions:

    match number_of_lines_of_a_file() with
    | x -> x
    [ exception File_not_found -> 0
This makes it really easy to create alternative versions of functions, using exceptions or option types:

    match number_of_lines_of_a_file_opt() with
    | Some x -> x
    | None -> raise File_not_found

    match number_of_lines_of_a_file_exc() with
    | x -> Some x
    | exception File_not_found -> None
flow control is always impacted by errors. rust and go just don't hide that on you with a giant 'go to random location in the stack' capability.
Agreed. Exceptions tend to trivially respect the Liskov substitution principle and generally hold little state besides a debugging string. It's when you subclass an object as a method of code reuse that you start running into problems.
Arguably, subtyping in am OO language should either be signatures/interfaces only, or you should go full blown multiple inheritance for everything, as with the Fortress language.
Golang ide struggle with answering what implements this interface. The compiler obviously handles that fine

It makes it difficult to jump into an unfamiliar project

Assuming that’s what you mean by signature/ interfaces

C++ is much more complicated than go in that regards and my IDE generally does not have trouble with it
But this is an issue with tooling.

IntelliJ with Java/Kotlin does a great job here.

Unsolved AFAIK, Reducing the ergonomics of the language which is an important point
Is Fortress the same language that required exponential time constraint solving for its type system?
Type checking is not exponential last I checked. However, languages with global type inference have exponential behaviour when inferring types for some pathological programs.