Showing posts with label mix inference. Show all posts
Showing posts with label mix inference. Show all posts

Friday, October 1, 2010

SKI!

Note: I'm actually not much of a skier, being as I am more interested in sliding sideways...

So, a while ago I was wandering the halls of the internet and came across this fun post, an implementation of the SKI combinator calculus at the type level in Scala. So I asked myself, "would this work in Mix?" The answer is "yes", which causes me a bit of consternation given that I'd like to do type inference for Mix. But anyway, let's have a look.

First, what is SKI? It's a set of combinators S, K, and I, along with rules for evaluating or rewriting them, that happens to be equivalent, in some sense, to the untyped lambda calculus. Evaluating expressions composed of these combinators (and variables x, y, and so on) is pretty simple: I x evaluates to x, K x y evaluates to x, and S x y z evaluates to x z (y z). So, for instance, S K x y evaluates first to K y (x y) by the S rule, and then to y by the K rule. And S K I I evaluates to I by S and then K. Note that we can actually simulate I using, for example, S K K, so we can only consider S and K going forward.

The Scala encoding given in by Michid is fully at the type level. We define several classes: one for each combinator, and a few representing variables. Each class has a "member" type corresponding to application and evaluation. The former is parameterized by an "argument" type. So if we want to construct a type representing I x, and we have defined I and x, we simply access the "application" member type of I, parameterizing it with the type x: Ix = I#apply[x].

Next, if we want to evaluate it (and thereby get back x), we simply access the "evaluation" member type of Ix: x' = Ix#eval.

An important aspect of the implementation given is that it actually performs evaluation as it is possible. By this I mean that any time we try to apply a reducible term t to another s, as in t#apply[s], we first reduce t and only then access the member type of the result of the reduction. So all of the types that correspond to reducible terms have a definition for apply that looks like this:

    type apply[x <: Term] = eval#apply[x]

Apologies for the lack of Scala syntax highlighting.

It first evaluates itself by accessing its own eval, and then applys the argument by accessing apply member type of the result of evaluation, parameterized by x. And it results in types representing terms that, after construction, have only one thing left to reduce.

So, how does this play out in Mix? Quite nicely, in fact, because in Mix we don't need to lift all of our computation into the type level in the same way. In Scala we created part of a class hierarchy (Term, S, etc.) for implementing SKI, the same as we would have if we wanted to evaluate it at runtime. But we didn't write the revelant Apply and Evaluate methods; instead we reflected them as types. In Mix we will implement these methods as usual, and find that the inference algorithm will evaluate our code correctly at compile time. That is to say, we can reflect only our data as types, but implement our functions on that data as methods over instances of types:

    // Base term class with default implementations.
    class Term
    {
        Apply(z) { return new Application(this, z); }
        Eval() { return this; }
        operator+(z) { return this.Apply(z); }
    }
    
    // Applications
    class Application : Term
    {
        var x; var y;
        new(x, y){ this.x = x; this.y = y; }
        ToString() { return "(" + x + " " + y + ")"; }
    }
    
    // Our combinators: partial applications of combinators override
    // Apply; complete applications override Eval as well.
    class K : Term
    {
        Apply(x){ return new Kx(x); }
    }
    
    class Kx
    {
        var x;
        new(x){ this.x = x; }
        Apply(y){ return new Kxy(this.x, y)
        ToString(){ return "(K " + this.x + ")";
    }
    
    class Kxy
    {
        var x; var y;
        new(x, y){ this.x = x; this.y = y; }
        Apply(z){ return this.Eval().Apply(z); }
        Eval(){ return this.y; }
        ToString(){ return "(K " + this.x + " " + this.y + ")";
    }
    
    // And so on for S, Sx, Sxy, and Sxyz, as well as I and Ix.
        
    // Constants have a specialized ToString; we need separate
    // classes so we can distinguish between an Constant("C")
    // and Constant("D") at the type level.
    class Constant : Term
    {
        var name;
        new(name) { this.name = name; }
        ToString() { return this.name; }
    }
    
    class C : Constant
    {
        new() : Constant("C") {}
    }
    
    class D : Constant
    {
        new() : Constant("D") {}
    }

A few comments on the code itself. We use inheritance to build a term hierarchy, as you might find in C# or Java, giving default implementations of Eval and Apply, as well as of operator+ which is implemented in terms of the latter. We also define ToString for printing the results at runtime; note that we frequently make use of the fact that Mix.String.operator+ invokes ToString on its argument.

Next, a few comments on the intentions behind the code. The most important is that we must use a new class for each different constant we want to represent, so that the type of a term actually reflects the structure of the term. That is, when we write var x = new Application(new C(), new D()); we want that they type of x is something like Application<C, D>, written in a C#-like syntax. Next, we also ensure that instances of C and D are different at runtime in their value (that is, what is printed by ToString).

Now we can construct a combinator term and evaluate it. We'll evaluate S K I c to c. Unfortunately we need to ask Mix to print the type of the result of evaluating the term using some "internals", namely the as ? operator, which prints a term's inferred type. Then we print it out, demonstrating that we can evaluate at the type level, but we're also playing with value level data representations.

    var S = new S();
    var K = new K();
    var I = new I();
    var c = new C();
    var d = new D();
    
    Int main(String args)
    {
        var term = S + K + I + C;
        var result = term.Eval() as ?;
        IO.print(string(term) + " => " + result);
        return 0;
    }
When we compile it we would get something like this log message, if I had a prettier pretty-printer for types:
ski.mix(10) : Log : Expression 'term.Eval()' has type 'C'.

And there you have it: type inference (but not type checking, though that doesn't make much sense as we haven't a way to write down types with methods, classes, and so on) in Mix is undecidable. Should we care? Do you?

Note: I haven't actually tested this code in a while, as my current version of Mix is in the midst of a rewrite!

Thursday, January 21, 2010

This post is brought to you by the Greek letter...

So, a long time ago I said that a Mix alpha release is coming, and it really is, I swear. Here are some examples of Mix code that the implementation can currently compile correctly.

1: This is an example of some code that cannot be handled under Hindley-Milner style inference. Specifically, notice that in the body of twice we apply f to the type of its own result. This requires that f have type 'a -> 'a, and therefore that twice have type ('a -> 'a) -> a'. Clearly in our application of twice the argument pair doesn't have such a restricted type.

twice(f)
{
    return function(x){ return f(f(x)); };
}

pair(x)
{
  return new Pair(x, x);
}

Int main(String args)
{
  var p = twice(pair)(1);
  return p.First.First;
}

2: This example shows how cool container classes are in Mix, at least to me. I love homogenous containers because they (well, the type system) take care of ensuring I don't do anything too stupid with its elements. However, in a language like C# or Java you must introduce an (often artificial) interface in order to group objects of different type but related functionality in such a container -- otherwise you just have to relegate yourself to containers of object, and live with the lack of type safety. Here Mix infers what operations are possible on all elements of the array. The error arrises because Mix infers that the array possibly contains both integers and strings, the latter of which do not implement the negation operator -operator.

Another note: arrays are enumerable (which just means that they implement GetEnumerator correctly), and enumerables implement ToString by simply concatenating the string representations of all of their elements. So as long as all of the elements of an enumerable implement ToString, the enumerable itself does as well, which is how we can call IO.print on the array.

Int main(String args)
{
  var a = new Array(10);
  for(var i = 0; i < a.Length(); i++)
  {
    a[i] = i;
  }
  a[random(10)] = "Hello!";

  IO.print(a);  //Enumerable.

  a[4] = -a[4]; //Error, strings cannot be negated.
  return 0;
}

3: Here's a funny example in which iterating to a fixed point proves critical. Essentially f gets a type that should be interpreted as "either a pointer to an integer, or a pointer to a pointer to a string, or a pointer to a pointer to a pointer to an integer, or..." Which essentially means that the only operations you can really perform on f are ToString and Get (which gets the value of a pointer).

Int main(String s)
{
  var f = new Ptr(0);
  var g = new Ptr("Foo!");
  
  for(var i = 0; i < 10; i += 1)
  {
    var temp = f;
    f = new Ptr(g);
    g = new Ptr(temp);
  };
  
  IO.print("f: " + f);
  IO.print("g: " + g);
  return 0;
}

I wanted to put a few more up, but it is time to get back to work.

Sunday, November 29, 2009

Turtle Tower

My good friend RT painted this the other day -- little did he know that today Infinite Turtle Theory would actually be relevant to what I want to talk about:

Anyway, here's the thing: in Mix I want every first class value to be an object. That is, instances of classes are objects, literals like 7 and true are really just instances (of Int and Bool, respectively) and so are objects. Functions are just objects with a method named op(). And methods are just functions that hold a reference to this -- recall this discussion on implementing methods. (This is a bit circular, I know; here be our first stack of turtles).

But, remember I said first class values; not everything you deal with in Mix is first class. Classes themselves (not their instances), control flow constructs (like if, for instance), and instance members are all examples of parts of Mix that are not first class. You can't pass a class around, modify it, and then create an instance from it. You can't pass a statement to a function (though it might be fun to try that out; I'll make a note of it). And you can't write the following code:

class Foo
{
  public var Bar = 7;
}

getField(o, f)
{
  return o.f;
}
Int main(String args)
{
  return getField(new Foo(), Bar);
}

Again, it might be interesting to allow that kind of first class status for member names in a statically typed language like Mix; I know other languages support such kinds of things.

Back on track, we want everything to be an object but this leaves us with a challenge: how do actually represent things like integers, reals, strings, and so on? For booleans we might try the following:

public Bool
{
  ToString()
  {
    if(this.IsTrue() is not null)
      return "true";
    else
      return "false";
  }
  
  operator==(other)
  {
    var t1 = this.IsTrue();
    var t2 = other.IsTrue();
    if(t1 is not null && t2 is not null)
      return new True();
    else if(t1 is null && t2 is null)
      return new True();
    return new False();
  }
}

class True : Bool
{
  IsTrue()
  {
    return this;
  }
  
  IsFalse()
  {
    return null;
  }
}

class False : Bool
{
  IsTrue()
  {
    return null;
  }
  
  IsFalse()
  {
    return this;
  }
}

Here we managed to define booleans in the language, though our technique seems a bit hackish; what about that "true" we left in there? Instead of trying to find a way of implementing each of these "ground types" with pure Mix, it would be nice if they were somehow "built-in" to the language. On the other hand, I'd rather not have to special case them all over the place, and it would be great if we could grow the set of built-ins easily. So, what is at the bottom of this stack of turtles (our second, for those of you who are counting).

Enter abstract classes. An abstract class is one whose purpose is to provide a "binding" to a built-in type (one that is likely implemented in C#). For instance, we could have an abstract class binding System.Int32, one binding System.String, and so on. The binding is like an interface between the C# implementation and the Mix type system. An abstract class generates no code when it is compiled; this "interfacing" is it's only role.

To see how it works, consider the following binding for System.String:

abstract class String
{
  operator==(o)
  {
    return null as Bool;
  }
  
  operator[](index)
  {
    index.ToInt() as Int;
    return null as Bool;
  }
  
  Join(s)
  {
    s.ToString() as String;
    return null as String;
  }
}

Lot's of code, all involving this new operator as. This operator isn't the same as the C# as operator (and therefore perhaps it should get a different name, though as you'll see it is somewhat fitting). It's behavior at compile time is that it checks that the left side argument has only exactly the type on the right; if so it returns a typeset consisting of a new instance of the type on the right. At runtime the as expression just returns its left side.

Given this description, let's consider the definition of operator[]. Type checking an invocation of this method with an argument index entails checking that index implements ToInt, and that the result of invoking ToInt is in fact an instance of type Int; if this is the case then the return type is Bool; otherwise an error is returned.

So, other than making it very easy to bind simple types, what do abstract classes get us? Well, consider binding an array:

class Array
{
  private var elements;
  
  new(index)
  {
    index.ToInt() as Int;
  }
  
  operator[](index)
  {
    index.ToInt() as Int;
    return this.elements;
  }
  
  operator[]=(index, value)
  {
    index.ToInt() as Int;
    this.elements = value;
    return;
  }
  
  // Etc.
}

What's nice about this binding is that we can use an instance variable to track the type of the array. So, if we have an array, and add a few items to it, we can verify that any object accessed from the array is used in the correct way; this of course restricts the array so that all elements are assumed to be of the same "type", so that we don't allow a particular operation on an element unless all elements can perform that operation (remember our definition of correctness).

Int main(String args)
{
  var a = new Array(10);  //Array starts out with null in each position.
  for(var i = 0; i < a.Length(); i++)
    a[i] = "Hello!";
  a[0] = 7;
  a[2] = true;
  
  foreach(var e in a)
    print(a.ToString());  //OK: Int, Bool, and String all implement ToString.
    
  a[6].ToLowerCase(); //Error: Int and Bool do not implement ToLowerCase.
}

So that's how the current incarnation handles the second infinite stack of turtles. We'll see how to deal with the first infinite turtle tower (namely, that a function is an object with a particular method, but a method is just a function -- which is an object with a particular method, but a method is just a function -- this is getting old already) next time.