I remember when I was (really) young, the excitement of the discovery when learning a programming language. It was BASIC first. That amazing feeling of being able to instruct a machine to execute your instructions and produce a visible result! Then came the mysterious Z80 assembly, with the incredible power of speed at the cost of long, tedious hours of hand-writing machine codes and the void sensation when the program just crashed with nothing but your brain to debug it.
A few years later, I was introduced to C. A shiny new world, where the promise of speed paired up with the ease of use (well, compared to hand-written assembly, it was easy indeed). And later on, C++. Up to this point, it seemed like a positive progression; each step had definitive advantages over the previous one, no regret or hesitation in jumping onto the new cart.
In the next years, I have been exposed to many other languages (Prolog, Lisp, PHP, bash, and Java, to name a few), but C++ always had a clear advantage over them, so I started settling on what seemed like the best language ever – C++. No problem I faced, needed anything but some amount of well-crafted lines of C++ code.
Fast forward a few years in the new millennium, and I was forced to work with Scala. It was hard at the beginning – the language had strange and foreign constructs, the code base I inherited was heavily influenced by functional programming style, and I felt lost. I couldn’t understand why you needed those pesky contraptions when C++ provided such easy and engineered solutions. All those talks about monads, monoids, and functors seemed only smoke in the eyes to hide something simpler.
On the other hand, I couldn’t turn around or go away – I had that code base and I had to maintain and evolve it. So I had to insist. Slowly, a bit at a time, I came to appreciate different ways to do things and came to the realization that C++ is lacking some serious stuff. Granted, C++ is Turing-complete, so it could solve any computable problem (as C and BASIC are), but C++ limitations make it harder than needed to solve problems.
It took quite a long time and a good effort before coming to the realization that Scala was indeed a better language than C++. Despite some sharp edges in a few areas, the language was better engineered and better suitable for expressing algorithms and writing programs.
When you become proficient in just one language, you become blind to what better languages offer beyond the features provided in the language you master. In this condition, it is easy to refuse learning new languages based on the false belief that they have nothing you need, that features beyond what you are fluent in are useless or worthless.
In this series of posts, I’ll present some features I found valuable in Scala and that led me to the conclusion that, from the programmer’s standpoint, Scala is a better language than C++, providing plenty of tools for making robust and reliable programs. I don’t consider Scala the best language ever, because I don’t think such a language exists. If you need high-performance computing in constrained resources, Scala is likely not the language to look at. Nonetheless, I believe that programmers should periodically be bumped a bit away from their comfort zone, from The Language they use every day, and learn new ways and new concepts. This will help them become better programmers, even with their pet language.
Automatic Selection of Parameter Passing Mode
Initially, C had only one way to pass parameters: by copy. When large structures, whose copy was too expensive, were involved, a pointer to their address was passed instead. But the language still passed the pointer by copy. C++ made things more complex, both in terms of the different ways to pass arguments and in providing parametric types (templates).
Deciding whether to pass a known type by reference or by copy to a standard function can be non-trivial in some cases; taking the same decision for a function template is much harder, and doing it correctly may involve some fancy template metaprogramming, because beforehand you can’t tell whether is faster to copy type T, or access it via a reference.
Scala solves the problem by letting the type of the structure know how to pass it. Integers, floats, and Booleans are likely to be better with a copy, so they all inherit from the AnyVal base class, which means “pass by copy”.
To be totally honest, Scala is not flexible on this. Only a fixed number of built-in types inherit from AnyVal, while every other type, including user-defined types, inherits from AnyRef. This means that a user-defined class that contains a couple of Char
s derives from AnyRef, causing it to be passed by reference, when, possibly, passing them by copy is faster. Nonetheless, this feature is somewhat liberating – the passing mode is fixed, and you don’t have to scratch your head on *
, &
, &&
or nothing.
Sensible Lambda Functions Declaration
If you are a reader of my blog, you already know what I think of the C++ lambda declarations, so I won’t repeat myself (for those who are not regular readers, it suffices to know that I am not fond at all of C++ lambda syntax). Let’s consider a dummy filter that selects all the even numbers in a range:
auto f = some_range | std::views::filter( []( auto n ){ return n % 2 == 0; } );
BTW, you have plenty of options to get it wrong in the lambda part alone – my favourite wreaking havoc error is missing the semicolon of the return. If you are a newcomer to the language (or the feature), you may also wonder what the empty array at the beginning means. Now compare with Scala:
val f = some_container.filter( _ % 2 == 0 )
That’s all. No capture clause, no braces, no arguments, no type definition, no return statement. This lambda contains only what is strictly needed. The underscore tells the compiler where to put, in the expression, the lambda argument, saving you from thinking of a sensible name. Both argument type and return type are determined from the context.
But if you need or want, Scala offers a fully verbose syntax to make things more explicit –
val f = some_container.filter( ( n : Int ): Boolean => n == 2 )
It can be noticed that Scala doesn’t explicitly list the captured variables. In C++, this may be needed because the way these implicit arguments are passed to the lambda has to be chosen by the programmer, while in Scala, this is dictated by the type itself. Moreover, the Scala GC-based memory management guarantees that objects will be around until needed.
There have been some proposals for C++ lambda simplification (such as Abbreviated Lambdas for Fun and Profit), but eventually they all got rejected by the standardization committee. My impression is that C++ has grown so complex that getting this kind of change in a safe and sound way is utterly hard.
Every Object goes to the Heap
Although not entirely true (see the section Automatic Selection of Parameter Passing Mode), this is an important difference between C++ and Scala. I know that by reading this, every seasoned C and C++ programmer would rip their clothes screaming about performance and cache misses, but… please bear with me and consider the benefits.
Indeed, there are at least two benefits –
- You don’t have to worry about how to pass an object (in C++, you can pass by copy, by reference, and r-value reference, and sometimes it is not obvious which one to pick).
- There is no risk of slicing objects – this means that the Liskov Substitution Principle may fully work everywhere.
I already wrote extensively about the first, so let’s go deeper into the second one. Liskov Substitution Principle (AKA LSP, and, notably, the ‘L’ in SOLID) states that a sound inheritance design lets the programmer replace, everywhere in the code, objects of the base class with objects of the derived class.
In C++, the application of this principle is hampered by handling object instances directly rather than via references. Consider the following snippet –
class Base { ... }; class Derived { ... }; Base& operator=( Base& dest, Base const& source ) { if( &dest != &source ) { // for each field in dest : dest.field = source.field; } return dest; } void f() { Derived d1{ ... }; Derived d2{ ... }; // ... d2 = d1; // slice!! }
The assignment operator, if invoked on a derived class, would slice it, since the fields specific to the derived class won’t be copied. The example is evident, but slicing may be subtle in a large codebase. Consider also that declaring a container of base classes, doens’t work if we use it for derived classes for this very same problem. We need to use pointers/smart pointers with more verbose and cumbersome coding.
Conclusions
Enough for the first part of this series. I’m pretty sure you are still unimpressed. Maybe the lambda syntax and the absence of slicing are the most interesting parts. But they could be balanced by the lack of control over where objects are allocated. So bear with me, we’ve just started, in the next installments we’ll see plenty of interesting features.