Tuesday, July 20, 2010

When CONSTants Vary

/*

Occasionally somebody will suggest that statically typed language X (Java, C#, Scala, F#, whatever) would be better off with C++'s "const", a type qualifer that supposedly makes objects immutable in certain contexts[1]. I'm not convinced. Const requires a fair amount of work and provides fairly small guarantees. This post is a bit of literate C++ program demonstrating some of the weaknesses in const.

There are a couple of holes that I want to dismiss quickly. First there's the weakness that const can be cast away. But hey, the C and C++ motto is "casting away safety since 1972." Blowing your leg off with a cast is half the fun of using the C derived languages. Second, there's the useful, if slightly misnamed, weakness of the "mutable"[2] keyword which can be applied to a field and says "this field can be mutated even in const contexts." Mutable is useful for things like memoizing.

But even ignoring those very deliberate issues, there are still a couple of fundamental weaknesses in how C++ deals with const.

For my example I'm going to use a simple toy class, a Counter that can be incremented (mutating its state) and then queried to see what the current count is.

*/

#include <iostream>

using namespace std;

class Counter {
   int _count;

public:
   Counter() : _count(0) {}
   void inc() {
      _count++;
   }

/*

A perfectly good use of const. The count() query doesn't change the state of this Counter.

*/

   int count() const{
     return _count;
   }
};

/*

Indirection

So far, so good. But we can easily destroy const with a bit of indirection. Imagine I need something that holds a pair of Counters and, for whatever reason, I need to do so via pointers.

*/

class CounterPair {
   Counter *_c1;
   Counter *_c2;

public:
   CounterPair() {
     _c1 = new Counter();
     try {
        _c2 = new Counter();
     } catch(...) {
        delete _c1;
        throw;
     }
   }

   // technically should have copy constructor 
   // and operator=, but I'm too lazy

   ~CounterPair() {
      delete _c1;
      delete _c2;
   }

/*

These two methods query the state of the CounterPair and are safely const

*/

   int count1() const {
     return _c1 -> count();
   }

   int count2() const {
     return _c2 -> count();
   }

/*

Up to this point everything is golden. But these next two methods modify the state of the CounterPair, yet are marked const.

*/

   void inc1() const {
     _c1 -> inc();
   }

   void inc2() const {
     _c2 -> inc();
   }
};

/*

What gives? Well, C++ doesn't appear to understand the that state and memory are two different things. I didn't modify the memory that lies directly in one CounterPair envelope but I'm clearly modifying the state of the CounterPair. "Transitive constness" is one area where the D programming language does much better than C++.

Aliasing

An even more subtle issue happens with aliasing. The following function does not modify its first argument but does modify its second. The first is marked const.

*/

void incSecondCounter(Counter const &c1, Counter &c2) {
  c2.inc();
}

int main(int argc, char** argv) {
   Counter c;

/*

But if we create a bit of aliasing then c1's referent gets modified

*/

   Counter const &c1 = c;
   Counter &c2 = c;
   cout << c1.count() << endl;
   incSecondCounter(c1, c2);
   cout << c1.count() << endl;
}

/*

In this toy program that looks silly, but in larger programs aliasing can be quite subtle. And aliasing is a tricky, tricky problem to deal with well at the type level so even the D language will have an equivalent hole in const. At best what const says here is that "the object won't be modified via that particular reference," which is a heck of a lot weaker than "the object won't be modified."

Conclusion

Const correctness requires a lot of typing of both the push-the-buttons-on-the-keyboard kind and the keep-certain-static-properties-consistent variety. Yet it gives back relatively little. Even without poking holes in const using casting or "mutable" it's easy to get mutation in something that's supposed to be immutable. C++'s variety of const doesn't deal with indirection at all well and aliasing is a tricky nut that would substantially alter a language's type system. Is const worth adding to other languages? I'm not sure.

Footnotes

  1. C'99 also has a const keyword with very similar semantics. So if you replace references with pointers and strip out the thin veneer of OO this article would cover C as well.
  2. Misnamed because fields are generally mutable even without the keyword. But I suspect the committee would balk at "mutable_even_under_const."

*/

Friday, July 9, 2010

Code Monkeyism's Post Is Unfit For Serious Reading

Stephan Schmidt makes several points in Scala Is Unfit For Serious Development . Some are even valid. Unfortunately, the valid points are buried in over-generalizations and attacks on the attitudes of the developers and community.

It's unfit because the developers and the community are unwilling.

At the end of his little diatribe he thanks 2 members of the community for helping him with his problems. Sounds pretty willing to me.

1. The developers are interested in language research and writing papers, not in making a language for real development

So far from true. Yes they write papers. But real development is very much a goal of Scala. If it weren't then Java interoperability would be ditched yesterday. Java interop is a giant albatross around Scala's theoretical neck. Some examples of how Java interop fights Scala: null sucks, the jvm doesn't do general tail calls, structural types are hacked in using reflection, and traits/mixins require a rube-goldberg copy mechanism. Java interop is only a practical concern for real programmers who want to get stuff down now with existing libraries.

2. The version hell that is 2.8 - calling this version 2.8.0 is deceptive. As Python (3000) or Ruby (2.0) this should have been a major version called 3.0

Some truth. Ruby made nasty breaking changes in 1.9.1 and Python isn't immune to them. None-the-less, it really should be called Scala 3.0 to communicate the extent of the changes.

3. The developers do not care about binary compatibility, deprecated, soft migration or API compatibility. Things are changed on a whim - from one RC to the next, and they change major parts of the language/libraries in point releases.

There is a significant technical challenge to maintain binary compatibility given the fact that the JVM has no multiple-inheritance mechanism. But that shouldn't be confused with a lack of caring. Martin Odersky has promised that the next major release will focus on solving the binary compatibility issue once and for all. Deprecation - could have been done better. Definitely. Then again, Schmidt is playing with beta software where API INcompatibility are pretty much guaranteed.

4. They remove and add classes just as they see fit.

A repeat of point 3.

5. The community loves cutting edge. Instead of fixing bugs they upgrade to all new released RCs - so have you. It's called cutting edge for a reason: You'll cut yourself. And it's called bleeding edge because you will bleed.

An overgeneralization. "The community" is apparently represented by the library causing Schmidt his problem. Other developers seem to be more careful. lift, for instance, has been very careful about its migration policy and just released version 2.0 against the stable version of Scala.

Conclusion

Scala's binary compatibility sucks due to a real technical challenge interoperating efficiently with the JVM. The 2.8 series is really 3.0. Deprecation could be done better. One library that Schmidt used decided to fix a bug and compile against a beta version of Scala. Schmidt jumped into the beta version. A small bit of versioning hell ensued. From this Schmidt concluded that the Scala development team do not care and that Scala is unfit for development.

In the meantime, back in reality, the small Scala development team is continuing to move forward with a great language. The community continues to put out awesome libraries. Businesses like Novell, Twitter, and FourSquare continue to make bets on Scala.

Is Scala flawed. Hell yes. But if you think it's a great language then contribute. Help make it and its ecosystem better. Help the developers of libraries maintain bug fixes on stable branches, or help test for breaking changes and push for a deprecation model, or contribute constructively to the binary compatibility challenge. Don't take potshots against the developers from the sidelines.