Perils of Constructors

One of my favorite blog posts about Rust is Things Rust Shipped Without by Graydon Hoare. To me, footguns that dont exist in a language are usually more important than expressiveness. In this slightly philosophical essay, I want to tell about a missing Rust feature I especially like: constructors.

What Is Constructor

Constructors are typically found in Object Oriented languages. The job of a constructor is to fully initialize an object before the rest of the world sees it. At the first blush, this seems like a really good idea:

  1. You establish invariants in the constructor.
  2. Each method takes care to maintain invariants.
  3. Together, these two properties mean that it is possible to reason about the object in terms of coarse-grained invariants, instead of fine-grained internal state.

The constructor plays a role of induction base here, as it is the only way to create a new object.

Unfortunately, theres a hole in this reasoning: constructor itself observes an object in an inconsistent state, and that creates a number of problems.

Value of this

When the constructor initializes the object, it starts with some dummy state. But how do you define a dummy state for an arbitrary object?

The easiest answer is to set all fields to default values: booleans to false, numbers to 0, and reference types to null. But this requires that every type has a default value, and forces the infamous null into the language. This is exactly the path that Java took: at the start of construction, all fields are zero or null.

Its really hard to paper over this if you want to get rid of null afterwards. A good case study here is Kotlin. Kotlin uses non-nullable types by default, but has to work with pre-exiting JVM semantics. The language-design heroics to hide this fact are really impressive and work well in practice, but are unsound. That is, with constructors it is possible to circumvent Kotlin null-checking.

Kotlins main trick is to encourage usage of so-called primary constructors, which simultaneously declare a field and set it before any user code runs:

class Person(
  val firstName: String,
  val lastName: String
) { ... }

Alternatively, if the field is not declared in the constructor, the programmer is encouraged to immediately initialize it:

class Person(val firstName: String, val lastName: String) {
    val fullName: String = "$firstName $lastName"
}

Trying to use a field before initialization is forbidden statically on the best effort basis:

class Person(val firstName: String, val lastName: String) {
    val fullName: String
    init {
        println(fullName) // error: variable must be initialized
        fullName = "$firstName $lastName"
    }
}

But, with some creativity, one can get around these checks. For example, a method call would do:

class A {
    val x: Any
    init {
        observeNull()
        x = 92
    }
    fun observeNull() = println(x) // prints null
}

fun main() {
    A()
}

As well as capturing this by a lambda (spelled { args -> body } in Kotlin):

class B {
    val x: Any = { y }()
    val y: Any = x
}

fun main() {
    println(B().x) // prints null
}

Examples like these seem contorted (and they are), but I did hit similar issues in real code (Kolmogorovs zeroone law of software engineering: in a sufficiently large code base, every code pattern exists almost surely, unless it is statically rejected by the compiler, in which case it almost surely doesnt exist).

The reason why Kotlin can get away with this unsoundness is the same as with Javas covariant arrays: runtime does null checks anyway. All in all, I wouldnt want to complicate Kotlins type system to make the above cases rejected at compile time: given existing constraints (JVM semantics), cost/benefit ratio of a runtime check is much better than that of a static check.

What if the language doesnt have a reasonable default for every type? For example, in C++, where user defined types are not necessary references, one can not just assign nulls to every field and call it a day! Instead, C++ invents special kind of syntactic machinery for specifying initial values of the fields: initializer lists:

#include <string>
#include <utility>

class person {
  person(std::string first_name, std::string last_name)
    : first_name(std::move(first_name))
    , last_name(std::move(last_name))
  {}

  std::string first_name;
  std::string last_name;
};

Being a special syntax, the rest of the language doesnt work completely flawlessly with it. For example, its hard to fit arbitrary statements in initializer lists, because C++ is not expression-oriented language (which by itself is OK!). Working with exceptions from initializer lists needs yet another obscure language feature.

Calling Methods From Constructor

As Kotlin examples alluded, all hell breaks loose if one calls a method from a constructor. Generally, methods expect that this object is fully constructed and valid (adheres to invariants). But, in Java or Kotlin, nothing prevents you from calling a method in constructor, and that way a semi-alive object can escape. Constructor promises to establish invariants, but is actually the easiest place to break them!

A particularly bizarre thing happens when the base class calls a method overridden in the subclass:

abstract class Base {
    init {
        initialize()
    }
    abstract fun initialize()
}

class Derived: Base() {
    val x: Any = 92
    override fun initialize() = println(x) // prints null!
}

Just think about it: code for Derived runs before the its constructor! Doing a similar thing in C++ leads to even curiouser results. Instead of calling the function from Derived, a function from Base will be called. This makes some sense, because Derived is not at all initialized (remember, we cant just say that all fields are null). However, if the function in Base happens to be pure virtual, undefined behavior occurs.

Constructors Signature

Breaking invariants isnt the only problem with constructors. They also have signature with fixed name (empty) and return type (the class itself). That makes constructor overloads confusing for humans.

The problem with return type usually comes up if construction can fail. You cant return Result<MyClass, io::Error> or null from a constructor!

This is often used as an argument that C++ with exceptions disabled is not viable, and that using constructors force one to use exceptions as well. I dont think thats a valid argument though: factory functions solve both problems, because they can have arbitrary names and can return arbitrary types. I actually this to be an occasionally useful pattern in OO-languages:

  • Make a single private constructor that accepts all the fields as arguments and just sets them. That is, this constructor acts almost like a record literal in Rust. It can also validate any invariants, but it shouldnt do anything else with arguments or fields.

  • For public API, provide the necessary public factory functions, with appropriate naming and adjusted return types.

A similar problem with constructors is that, because they are a special kind of thing, its hard to be generic over them. In C++, default constructable or copy constructable cant be expressed more directly than certain syntax works. Contrast this with Rust, where these concepts have appropriate signatures:

trait Default {
    fn default() -> Self;
}

trait Clone {
    fn clone(&self) -> Self;
}

Life Without Constructors

In Rust, theres only one way to create a struct: providing values for all the fields. Factory functions, like the conventional new, play the role of constructors, but, crucially, dont allow calling any methods until you have at least a basically valid struct instance on hand.

A perceived downside of this approach is that any code can create a struct, so theres no the single place, like the constructor, to enforce invariants. In practice, this is easily solved by privacy: if structs fields are private it can only be created inside its declaring module. Within a single module, its not at all hard to maintain a convention like all construction must go via the new method. One can even imagine a language extension that allows one to mark certain functions with a #[constructor] attribute, with the effect that the record literal syntax is available only in the marked functions. But, again, additional language machinery seems unnecessary: maintaining local conventions needs little effort.

I personally think that this tradeoff looks the same for first-class contract programming in general. Contracts like not null or positive are best encoded in types. For complex invariants, just writing assert!(self.validate()) in each method manually is not that hard. Between these two patterns theres little room for language-level or macro-based #[pre] and #[post] conditions.

A Case of Swift

An interesting language to look at the constructor machinery is Swift. Like Kotlin, Swift is a null-safe language. Unlike Kotlin, Swifts null-checking needs to be sound, so it employs interesting tricks to mitigate constructor-induced damage.

First, Swift embraces named arguments, and that helps quite a bit with all constructors have the same name. In particular, having two constructors with the same types of parameters is not a problem:

Celsius(fromFahrenheit: 212.0)
Celsius(fromKelvin: 273.15)

Second, to solve constructor calls virtual function from an objects class that didnt came into existence yet problem, Swift uses elaborate two-phase initialization protocol. Although theres no special syntax for initializer lists, compiler statically checks that constructors body has just the right, safe and sound, form. For example, calling methods is only allowed after all fields of the class and its ancestors are set.

Third, theres special language-level support for failable constructors. A constructor can be declared nullable, which makes the result of a call to a constructor an option. A constructor can also have throws modifier, which works somewhat nicer with Swiftss semantic two-phase initialization than with C++ syntactic initializer lists.

Swift manages to plug all of the holes in constructors I am ranting about. This comes at a price, however: the initialization chapter is one of the longest in Swift book!

When Constructors Are Necessary

However, I can think of at least two reasons why constructors cant be easily substituted with Rust-style record literals.

First, inheritance more or less forces the language to have constructors. One can imagine extending the record syntax with support for base classes:

struct Base { ... }

struct Derived: Base { foo: i32 }

impl Derived {
    fn new() -> Derived {
        Derived {
            Base::new()..,
            foo: 92,
        }
    }
}

But this wont work in a typical single-inheritance OO language object layout! Usually, an object starts with a header and continues with fields of classes, from the base one to the most derived one. This way, a prefix of an object of a derived class forms a valid object of a base class. For this layout to work though, constructor needs to allocate memory for the whole object at once. It cant allocate just enough space for base, and than append derived fields afterwards. But such piece-wise allocation is required if we want a record syntax were we can just specify a value for a base class.

Second, unlike records, constructors have a placement-friendly ABI. Constructor acts on the this pointer, which points to a chunk of memory which a newborn object should occupy. Crucially, a constructor can easily pass pointer to subobjects constructors, allowing to create a complex tree of values in-place. In contrast, in Rust constructing records semantically involves quite a few copies of memory, and we are at the mercy of the optimizer here. Its not a coincidence that theres still no accepted RFC for placement in Rust!

Discussion on /r/rust.