Skip to content

Latest commit

 

History

History
206 lines (165 loc) · 4.81 KB

what_kotlin_could_learn_from_cpps_keyword_const.md

File metadata and controls

206 lines (165 loc) · 4.81 KB

What Kotlin could learn from C++'s keyword const

Let's say, in Kotlin we have some simple 2D-vector class. It has two methods, one (normalize) which mutates the object, and one (length) which does not:

import kotlin.math.sqrt

class Vector(private var x: Double, private var y: Double) {
    fun length() = sqrt(x * x + y * y)
    fun normalize() {
        val l = length()
        x /= l
        y /= l
    }
}

Imagine Vector being some kind of domain entity (with an ID, etc.), such that normalize needs to mutate it instead of just returning a new, immutable normalized Vector (value object).

We could now use it like that:

fun foo(v: Vector) {
    v.normalize()
}

fun bar(v: Vector) {
    println(v.length())
}

fun main() {
    val myVector = Vector(3.0, 4.0)
    foo(myVector)
    bar(myVector)
}

However, we might want to make sure bar can not mutate our object. It should only be allowed to call the non-mutating methods. To achieve this, we need to provide two different classes:

open class Vector(protected var x: Double, protected var y: Double) {
    fun length() = sqrt(x * x + y * y)
}

class MutableVector(x: Double, y: Double) : Vector(x, y) {
    fun normalize() {
        val l = super.length()
        x /= l
        y /= l
    }
}

Now we can adjust the rest of our code, such that the desired property of bar is satisfied:

fun foo(v: MutableVector) {
    v.normalize()
}

fun bar(v: Vector) {
    println(v.length())
}

fun main() {
    val myVector = MutableVector(3.0, 4.0)
    foo(myVector)
    bar(myVector)
}

MutableList and List in Kotlin's standard library follow a similar approach. Actually, Kotlin is in good company here. Other languages (not just Java) utilize similar mechanisms.

So, let's translate this straight into C++:

#include <cmath>
#include <iostream>

class vector {
public:
    vector(double x, double y): x_(x), y_(y) {}
    double length() {
        return std::sqrt(x_ * x_ + y_ * y_);
    }
protected:
    double x_;
    double y_;
};

class mutable_vector: public vector {
public:
    mutable_vector(double x, double y): vector(x, y) {}
    void normalize() {
        auto l = length();
        x_ /= l;
        y_ /= l;
    }
};

void foo(mutable_vector& v) {
    v.normalize();
}

void bar(vector& v) {
    std::cout << v.length() << std::endl;
}

int main() {
    mutable_vector my_vector(3.0, 4.0);
    foo(my_vector);
    bar(my_vector);
}

Looks fine. However, in a code review, this would raise a huge laugh because C++ provides a much better solution. Using the const keyword, one can let the compiler do the tedious work of providing two different interfaces!

It looks as follows:

#include <cmath>
#include <iostream>

class vector {
public:
    vector(double x, double y): x_(x), y_(y) {}
    double length() const {
        return std::sqrt(x_ * x_ + y_ * y_);
    }
    void normalize() {
        auto l = length();
        x_ /= l;
        y_ /= l;
    }
private:
    double x_;
    double y_;
};

void foo(vector& v) {
    v.normalize();
}

void bar(const vector& v) {
    std::cout << v.length() << std::endl;
}

int main() {
    vector my_vector(3.0, 4.0);
    foo(my_vector);
    bar(my_vector);
}

By marking vector::length as const, but not vector::normalize, the compiler knows which member functions can be called depending on the const qualification of an instance or a reference to one.

bar now takes a reference-to-const parameter, which produces the exact effect we wanted, i.e., v is immutable and trying to call v.normalize() in its body would result in a compile-time error:

void bar(const vector& v) {
    v.normalize();
}

Error (generated by clang++):

error: 'this' argument to member function 'normalize' has type 'const vector', but function is not marked const
    v.normalize();
    ^

🎉

Good old C++, despite all its idiosyncrasies, seems to be doing something right here. :)

(Rust too.)

I'd love to hear your feedback.


Edit: The Kotlin community seems to already discuss such a possible feature (1, 2, 3), however, it seems to be quite tricky to integrate it into the language. I hope somebody finds a clean way, because I'd really love to have this in my day-to-day work with Kotlin.