Java / C# / Python / Ruby so they fall in the same bucket as Go I assume?
For starters "without null" is a myth. They both have null. They have to; there is no other practical option. The JDK uses null all over the place, so you need to have null in order to talk to the JDK.
Now, they do still have mechanism to make null easier to handle. And they're both pretty impressive designs. (Especially Kotlin's, though I haven't tried Scala 3 yet so maybe I'm missing something wonderful there. Aesthetically, I just prefer "we're going to openly acknowledge it and tame it as much as we can" over "we're going to try to sweep it under the carpet.") But there's a sort of Amdahl's Law analogue hiding in that situation: the upper bound on how much practical null safety you can achieve is constrained by how much you can avoid relying on modules that were written in Java. And I know that's basically true of languages like Haskell, too, because you often have to rely on at least a little bit of code that was written in languages like C. But it doesn't preoccupy me the same way it does in Scala or Kotlin, where interacting directly with Java code is a much more everyday kind of affair.
But, at the other end of things, teams that were already using Java and want to start incorporating Scala don't have that option. And 10 year old Scala projects didn't originally have that option, and doing something about it now may be a lift on the scale of a complete rewrite.
Not just because of the null problem. It's also that option types push you toward a "conditional logic everywhere" way of doing things, because that's how you handle the options. That's all well and good and holy in a functional language, and perhaps even a procedural one. But it's the opposite of good object-oriented design.
To quote Dr. Mark Crislip, when you serve cow pie with apple pie, it does not make the cow pie better. It just makes the apple pie worse.
BurgerWithFriesMeal subclasses BurgerMeal?
"Object oriented design", in the religious sense, is an obsolete 1980s fad that took a good idea (encapsulation of mutable state) to comical extremes.
The original idea of OO design was to strive to eliminate stateful idioms. Which is a slightly different idea than what we're used to. The state existed, but the point was that you were supposed to design your system so that objects didn't need to know - or even attempt to infer - information about other objects' state.
The whole intellectual lineage that includes pervasive use of explicit state querying and manipulation methods such as getters and setters could be characterized as a whole lot of procedural programmers collectively missing the point. It's right up there with when people over-use the State monad in Haskell, effectively doing their darnedest to Greenspun imperaive programming on top of a lazy functional language because they haven't quite internalized this new paradigm yet.
So what is good OO design? NPEs? Nil checks?
- nil
- None
- Some(nil)
- Some(non-nil)
As far as how to do good OO design, ideally you try to avoid explicit branching whenever possible, and instead use dynamic dispatch to decide what to do. In principle, if you've architected things well, you should generally be able to avoid conditional branching.An ironically useful example of how this works is Smalltalk's implementation of Booleans and conditionals. Smalltalk doesn't actually have an if statement. Instead, it has a Boolean class that defines three methods: ifTrue:, ifFalse:, and ifTrue:ifFalse:. Each takes one or two blocks, which are effectively anonymous functions.
And then the implementation is that the True subclass as an ifTrue: that executes the block, an ifFalse that doesn't, and an ifTrue:ifFalse: that executes its first argument. And the False implementation does the opposite.
This isn't meant to be an example of "look, OOP doesn't need conditional branching, just use {library implementation of conditional branching}," so much as a small, self-contained example of the kinds of ways that you can achieve conditional-style logic without explicit branch statements. A more real-world example might be something like having separate NotLoggedInUser and LoggedInUser classes that behave differently in relevant situations, rather than having an isLoggedIn field to have to keep checking at every use site.
The big thing working against us on this is that most the popular OO languages - C++, Java, Python, C#, etc - come from the same approach of trying to layer object-oriented features on top of a procedural core. In my not-so-humble opinion, this has been about as successful as more recent efforts to support functional programming by pulling a mess of functional features into existing OO languages. Technically it gets you somewhere, but the resulting language is not an ergonomic pit of success.
Kotlin code requires you to use Type? if the value can ever be null.
Java code that is annotated by @Nullable/@NonNull will automatically map to Type?/Type (and it is up of course to the developer to not fuck up their nullability promises.)
Java code that is not annotated is a Type!, and encourages you to be wary about what could happen.
The nullability story is miles better than Java.
Additionally, all Java code (including the one you wrote) is only optionally null checked. All Kotlin code _is_ null checked, no matter what.
- Know about it (first, big problem for many java shops)
- Integrate it with Gradle/Maven/Ant/yourbuildtool
- Enable the null checks because they are not enabled by default.
Compare this with "it's already in the compiler"