Tuesday, March 9, 2010

Innate Immutabilities

A quick note: at some point when implementing the Mix runtime I had occasion to write the Int class. I think I've mentioned abstract classes before; they act as bindings to .NET classes that exist in the runtime, or in user supplied code (in case you want to use your regular .NET code from Mix, and that code can't be automatically imported using the import statement because the types that you want associated with your code are too complicated for Mix to infer).

Anywho I was writing this class and came across a decision I had made a while ago that caused me to scratch my head. In Mix operators are just method calls, which gives an easy to understand form of operator overloading, whereby an operator invocation is just a method invocation on one of the arguments. Which argument is operator dependent, but I think almost all of them use the left argument, passing the right; of course, single argument operators like ++ are easy.

The decision that I made was to make certain operators "real" operators, not just syntactic sugar. For instance, I made operators ++ and -- real operators, and didn't just expand them from i++; to i = i + 1;. Seems cleaner, actually, to not treat it as sugar, so what's the big deal?

Well, in Mix all expressions are objects -- we've talked about functions being objects, methods being objects, and so on. We also have integers as objects, booleans as objects, and so on (note: I write "and so on" because I don't like the strangeness of punctuating an etc. -- I like putting my punctuation on the outside of parens, it just feels right). Another note: for now I want to ignore the performance implications of this choice, and focus on a clean language design. Another other note: classes themselves aren't objects, unfortunately; maybe in Mix version 2.

So integers are just instances of class Int, and we all love to increment our integers using i++;. So how do we write the .NET class implementing the Mix class Int? First, let's look at the abtract class itself:

abstract class Int
{
  public ToInt(){ return null as Int; }
  public ToString() { return null as String; }
  // More conversion operators...
  
  public operator+(i)
  {
    i.ToInt() as Int;
    return null as Int;
  }
  // More arithmetic operators...
  
  public operator++()
  {
    return null as Int;
  }
  // And anything else...
}

Let's take this in a few bites, my hungry friends. First, the syntax term as Type does two things: at compile time, it just checks that the term has only the given Type, and then returns a typeset containing just Type. At runtime, it does nothing, returning term; in an abstract class, it really does nothing, as class never has any code generated for it. The point of the null as Type idiom is to avoid writing new Int(), because it's not as obvious that it's just for compile time.

Given this description, you should be able to understand the definition of operator+; it checks that its argument is convertible to Int, and then returns an Int; the code to actually perform the addition is found in the .NET class implementing Int:

public class Int
{
  public int NativeInt;
  
  // Lots of methods...

  public operator_add(object other)
  {
    int i = Mix.Call(other, "ToInt").NativeInt;
    return new Int(NativeInt + i);
  }
  
  public operator_incr()
  {
    // What to do?
  }
}

So, in operator_add we invoke ToInt on the passed object and then assume that it worked, and that the result is an Int. We can safely assume these, because the Mix compiler verified that all code is well-typed. This lets us get the native .NET integer, add it to our own, and return a new wrapper with the sum of these two numbers.

Finally we come to the real issue: how should we implement operator_incr. Turns out the obvious way to implement leads to really crazy use cases! Let's say that the body is as follows:

public operator_incr()
{
  int original = NativeInt;
  NativeInt++;
  return new Int(original);
}

Well, that's craziness, because integers are almost always in everyone's minds, immutable. In the following, we pretty much all expect b to be 42:

var a = 42;
var b = a;
a++;

And of course with the above definition it isn't, not even a little. So, leave your integers immutable! But, how can we write the code, allowing for operator++ to be a real live operator? I'm not sure, but I certainly couldn't find a clean way (nor for operator+= and company).

One option is to say that when a class implements operator++ we call it, otherwise we interpret it in the "syntactic sugar style". The nice bit about this is that it also works for operator+= and friends, so we can, for instance, write a list that allows you to append elements to it using list += "new list item!". The problem is that it complicates both the mental model of the code, and also the implementation of the back end.

Maybe someone, somewhere has a thought or three?