Good post, for what is worth Java is slowly and painfully correcting course with features like records and project Valhalla. As with the other language improvements though we will have to live with the tech debt for decades to come…
While C# certainly has tech debt too, the specific feature the author mentions (value types) has been there since version 1. C# pioneered async/await in version 5, and has a plethora of functional features like generics, type inference and pattern matching now. Java is lagging behind like a wounded cow...
I've had thoughts along this line for a while. I think Scala does better than this article gives credit for; case classes are a significant step in the right direction, particularly post-Scala 3 (or with Shapeless in Scala 2) where you have many tools available to treat them as records, and you can distinguish practically between case classes (values) and objects with identity even if in theory they're only syntax sugar. It also offers an Erlang-style actor system if you want one.
In my dream language I'd push this further; case classes should not offer any object identity APIs (essentially the whole of java.lang.Object) and not be allowed to contain non-value classes or mutable fields, and maybe objects should be a bit more decoupled from their state. But for now I wouldn't let perfect be the enemy of good.
IMO the nice thing about Erlang and Elixir is their foundation of representing data is rock solid. Because data is fully immutable, you get a lot of nice things "for free" (no shared state, reliable serialisation, etc). And then on top of that you can add your interfacey, mutable-ish design with processes, if you want. But you will never have oddities or edge cases with the underlying data.
In contrast with languages like C++ and Java where things are shakey from the ground up. If you can't get an integer type right (looking at you, boxing, or you, implicit type conversions), the rest of the language will always be compensating. It's another layer of annoyances to deal with. You'll be having a nice day coding and then be forced to remember that int is different to Integer and have to change your design for no good reason.
Perhaps you disagree with Erlang's approach, but at least it's solid and thought-out. I'd take that over the C++ or Java mess in most cases.
That's also Clojure's approach. It's very nice to program with immutable data at a high level, but for certain things, you just need to use the computer's primitives as they actually are, with all the mess that entails. So, I would say we need languages like C++ and Java (but perhaps we should all be using Rust, which makes the mess much more manageable, despite bringing in a lot of complexity that programmers need to wrap their head around, by for example, making it really easy to represent data, and defaulting to immutability) even if it would be "nice" to avoid them where possible.
Erlang and Elixir (and Clojure), however, lack a static type system, which makes it really difficult to use them at large scale (I am happy if you can provide convincing evidence to the contrary - I just haven't seen any). There's Gleam, which is a beautiful, simple language, that has a very good type system, but unfortunately, it's a bit too simple and makes certain things harder, like serialization (e.g. https://hexdocs.pm/gleam_codec/).
Haskell and Ocaml are more usable, but for some reason are extremely niche. I don't think there's any popular language that's in the "statically typed, functional" school, which I think shows that humans just don't prefer using them (they have been hyped for decades now, yet they just never stick). Perhaps a new generation of these languages will change things, like Unison for example (which stays away from Monads but provide an arguably superior abstraction, Abilities - also known as Effects). I think I would love for that to happen, though as I said before: sometimes you still need to reach out for bare metal performance and you have to use Rust/C++/D or even Java.
> Because data is fully immutable, you get a lot of nice things "for free"
This. I discovered this by implementing Flow Based Programming[1] (FBP) in Erlang[2] - the FBP is best suited to languages that are message based and immutable data structures. When using a mutable language, messages are constantly being clone as they are passed around. This does not need to be done using Erlang/BEAM.
My feeling is that FBP is a much under-rated programming paradigm and something really worth exploring as an alternative to the current programming paradigms.
My take on FBP is that data is being passed to functionality, as opposed to function being passed to data - the functional programming paradigm, or data and function is passed around as is the case with OOP.
IMHO it makes sense to pass data to functions, particularly in the times of SaaS and cloud computing.
Having separate types for these is the problem; not the boxing. C#/IL/CLR handles boxing in a way that doesn't exhibit the problem. If your code is dealing with integers, they are never boxed. They are only boxed when you cast to a reference type such as object. As soon as you cast back to int, you are unboxing.
Java exhibits the problem in a big way because it doesn't have true generics, so you can't have (say) a list of integers or a dictionary with integer values, so they must always be boxed, so you need a separate “boxed integer” type to maintain type safety. In C# you can just use unboxed integers everywhere.
I didn't like how this essay misunderstood the design principles in Java's class files. With dynamic binding to the runtime, you can't know for certain the layout of a data structure in memory (e.g. what is the ideal memory alignment?). If the class file is untrusted, you can't even be sure you have a valid data structure in the first place. So allocating the array and then assigning elements one at a time is what you do.
Hi, Ruby seems to take the opposite approach, but does it still resolve the problems exposed in this article? I'm a beginner in development, knowing the basics of a few languages (mostly Python and JS). Being very sensitive to design and logic, I want to choose the right path forward. I want to learn a language that makes sense and help me to think rigorously.
Maybe it's just me but I find the complaint confusing and the suggested remedy absent in TFA, despite reading it twice.
Data comes from outside your application code. Your algorithms operate on the data. A complaint like "There isn’t (yet?) a format for just any kind of data in .class files" is bizarre. Maybe my problem is with his hijacking of the terms 'data' and 'object' to mean specific types of data structures that he wants to discuss.
"There is no sensible way to represent tree-like data in that [RDBMS] environment" - there is endless literature covering storing data structures in relational schemas. The complaint seems to be to just be "it's complicated".
Calling a JSON payload "actual data" but a SOAP payload somehow not is odd. Again the complaint seems to be "SOAP is hard because schemas and ws-security".
Statements like "I don’t think we have any actually good programming languages" don't lend much credibility and are the sort of thing I last heard in first year programming labs.
I'm very much about "Smart data structures and dumb code works a lot better than the other way around" and I think the author is starting there too, but I guess he's just gone off in a different direction to me.
clojure + lisps have this approach - everything is data. I encourage everyone to dabble in clojure. it changes your mindset. that when you go back to your regular language you will write better programs.
Apparently, Ruby takes the opposite approach: everything is object. I bet it leads to a completely different way to think and design, but at least it does not mix the two, is it right? Do you recommend it? (I have a choice to make between Ruby and JavaScript.)
> [Data] The schema gives us a fixed set of variants, over which you can of course write any function you want.
> [Objects] We have a fixed set of exposed operations, but different variants can be constructed (including an improved ability to evolve a variant without impacting clients).
No mention of the expression problem? The TL;DR is, sometimes[1] we want both. And sometimes[2] it’s an exceptionally good idea for a lot of slightly different sets of variants to coexist withn the same program, which there also isn’t really a satisfactory solution for.
No mention of C#, which of all the languages I've seen makes the best distinction for what he wants: structs/“value types” = data, classes/“reference types” = objects. He briefly mentions that Java is in the process of adding that; C# has had it since version 1. Now, 15 versions later, C# has acquired all the modern features from all paradigms of programming, including type inference, record types, and an incredibly full-featured pattern matching system. Java is laughably lagging behind.
He also praises async/await as a revolutionary leap forward for programming languages, but without a mention of the language that invented/pioneered it (that's right, it's C#, in version 5).
Now, I'm not going to sit here and claim that C# is perfect for all purposes. It has made mistakes which have to live on due to backwards compatibility. The mutability of structs is, in my opinion, one such mistake. But even so, of all programming languages I've seen, C# fits this author’s ideas like hand in glove.
Edit: the scenario he describes of putting a large array or other static data structure directly into the code is also better supported by modern C# compilers. For small structures you still get the element-by-element-adding bytecode, but there's also a serialization format that can bake large objects directly into the assembly.
> No mention of C#, which of all the languages I've seen makes the best distinction...
It's the "I've seen" that does the heavy lifting here. I'd guess it's C# for you only because you haven't seen Hejlsberg's predecessor to it, Borland Object Pascal / "Delphi".
Part of the mismatch here is that people discover programming languages as tools for thinking, maybe better than any other thinking tool they've ever encountered, and with the unique ability to verify one's thinking. Then they want to do all their thinking with this tool, including all of their design.
IME this leads you to consider more and more featureful languages which are worse and worse at actually building software, and never match the flexibility of a tool like pen and paper.
Poor design is your own fault. Write more, draw more, prototype more. You may need to develop your own notation, you may need to get better at drawing or invest in a drawing tool. You may need to learn another programming language, which you use only for prototyping.
The design of your system does not need to be perfectly represented in your source code. The source code needs to produce runnable machine code, which behaves in the ways that your design dictates, and that's the only link between the design, the code, and the running system. Programming languages today are pretty good at producing working software, but not very good at doing that and designing systems and communicating designs and documenting choices, etc.
OCaml’s object system is unfairly maligned by its users. It’s unfortunate because object types allow for some of the most useful kinds of polymorphism and people don’t reach for them often enough.
However, OCaml’s first class modules are frequently a useful and serviceable alternative. I think they’re a precise middle ground between what we here call “objects” and “data.”
The author dismisses C++ out of hand, but I really think it does a pretty good job of making this a non-issue. Want a structured value type? Sure, that's a struct with public fields by default, passed by value with automatic copy constructor and assignment functions. Want a mutable type that's encapsulated and needs to do something special to be cloned? Sure, that's a class passed by unique_pointer or reference, with non-default (or deleted) copy constructor and assignment functions and private fields by default.
Every language I've used since then feels like it makes this issue needlessly complicated and implicit.
C++ developer here (30+ years). C++ is really missing support for sum types. The Haskell JSON example shows how useful it would be to have native support for it. Yes you can build your own but it’s pages of boiler plate code.
25 comments
[ 4.3 ms ] story [ 60.5 ms ] threadIn my dream language I'd push this further; case classes should not offer any object identity APIs (essentially the whole of java.lang.Object) and not be allowed to contain non-value classes or mutable fields, and maybe objects should be a bit more decoupled from their state. But for now I wouldn't let perfect be the enemy of good.
In contrast with languages like C++ and Java where things are shakey from the ground up. If you can't get an integer type right (looking at you, boxing, or you, implicit type conversions), the rest of the language will always be compensating. It's another layer of annoyances to deal with. You'll be having a nice day coding and then be forced to remember that int is different to Integer and have to change your design for no good reason.
Perhaps you disagree with Erlang's approach, but at least it's solid and thought-out. I'd take that over the C++ or Java mess in most cases.
Erlang and Elixir (and Clojure), however, lack a static type system, which makes it really difficult to use them at large scale (I am happy if you can provide convincing evidence to the contrary - I just haven't seen any). There's Gleam, which is a beautiful, simple language, that has a very good type system, but unfortunately, it's a bit too simple and makes certain things harder, like serialization (e.g. https://hexdocs.pm/gleam_codec/).
Haskell and Ocaml are more usable, but for some reason are extremely niche. I don't think there's any popular language that's in the "statically typed, functional" school, which I think shows that humans just don't prefer using them (they have been hyped for decades now, yet they just never stick). Perhaps a new generation of these languages will change things, like Unison for example (which stays away from Monads but provide an arguably superior abstraction, Abilities - also known as Effects). I think I would love for that to happen, though as I said before: sometimes you still need to reach out for bare metal performance and you have to use Rust/C++/D or even Java.
This. I discovered this by implementing Flow Based Programming[1] (FBP) in Erlang[2] - the FBP is best suited to languages that are message based and immutable data structures. When using a mutable language, messages are constantly being clone as they are passed around. This does not need to be done using Erlang/BEAM.
My feeling is that FBP is a much under-rated programming paradigm and something really worth exploring as an alternative to the current programming paradigms.
My take on FBP is that data is being passed to functionality, as opposed to function being passed to data - the functional programming paradigm, or data and function is passed around as is the case with OOP.
IMHO it makes sense to pass data to functions, particularly in the times of SaaS and cloud computing.
[1] = https://jpaulm.github.io/fbp/index.html
[2] = https://github.com/gorenje/erlang-red
Having separate types for these is the problem; not the boxing. C#/IL/CLR handles boxing in a way that doesn't exhibit the problem. If your code is dealing with integers, they are never boxed. They are only boxed when you cast to a reference type such as object. As soon as you cast back to int, you are unboxing.
Java exhibits the problem in a big way because it doesn't have true generics, so you can't have (say) a list of integers or a dictionary with integer values, so they must always be boxed, so you need a separate “boxed integer” type to maintain type safety. In C# you can just use unboxed integers everywhere.
I've only seen this pattern used in contexts, where there is no filesystem to begin with.
Closure is different of course. But not more functional than Haskell for example.
Data comes from outside your application code. Your algorithms operate on the data. A complaint like "There isn’t (yet?) a format for just any kind of data in .class files" is bizarre. Maybe my problem is with his hijacking of the terms 'data' and 'object' to mean specific types of data structures that he wants to discuss.
"There is no sensible way to represent tree-like data in that [RDBMS] environment" - there is endless literature covering storing data structures in relational schemas. The complaint seems to be to just be "it's complicated".
Calling a JSON payload "actual data" but a SOAP payload somehow not is odd. Again the complaint seems to be "SOAP is hard because schemas and ws-security".
Statements like "I don’t think we have any actually good programming languages" don't lend much credibility and are the sort of thing I last heard in first year programming labs.
I'm very much about "Smart data structures and dumb code works a lot better than the other way around" and I think the author is starting there too, but I guess he's just gone off in a different direction to me.
> [Data] The schema gives us a fixed set of variants, over which you can of course write any function you want.
> [Objects] We have a fixed set of exposed operations, but different variants can be constructed (including an improved ability to evolve a variant without impacting clients).
No mention of the expression problem? The TL;DR is, sometimes[1] we want both. And sometimes[2] it’s an exceptionally good idea for a lot of slightly different sets of variants to coexist withn the same program, which there also isn’t really a satisfactory solution for.
[1] https://www.craftinginterpreters.stuffwithstuff.com/represen...
[2] https://nanopass.org/
He also praises async/await as a revolutionary leap forward for programming languages, but without a mention of the language that invented/pioneered it (that's right, it's C#, in version 5).
Now, I'm not going to sit here and claim that C# is perfect for all purposes. It has made mistakes which have to live on due to backwards compatibility. The mutability of structs is, in my opinion, one such mistake. But even so, of all programming languages I've seen, C# fits this author’s ideas like hand in glove.
Edit: the scenario he describes of putting a large array or other static data structure directly into the code is also better supported by modern C# compilers. For small structures you still get the element-by-element-adding bytecode, but there's also a serialization format that can bake large objects directly into the assembly.
It's the "I've seen" that does the heavy lifting here. I'd guess it's C# for you only because you haven't seen Hejlsberg's predecessor to it, Borland Object Pascal / "Delphi".
Sigh... Tempus fugit.
IME this leads you to consider more and more featureful languages which are worse and worse at actually building software, and never match the flexibility of a tool like pen and paper.
Poor design is your own fault. Write more, draw more, prototype more. You may need to develop your own notation, you may need to get better at drawing or invest in a drawing tool. You may need to learn another programming language, which you use only for prototyping.
The design of your system does not need to be perfectly represented in your source code. The source code needs to produce runnable machine code, which behaves in the ways that your design dictates, and that's the only link between the design, the code, and the running system. Programming languages today are pretty good at producing working software, but not very good at doing that and designing systems and communicating designs and documenting choices, etc.
However, OCaml’s first class modules are frequently a useful and serviceable alternative. I think they’re a precise middle ground between what we here call “objects” and “data.”
Every language I've used since then feels like it makes this issue needlessly complicated and implicit.