Thursday, December 31, 2015

Delegates - Part 1

Turns out it was delegates. Having lists without higher order functions was going to annoy me sooner rather than later, and allowing mutable state - even local mutable state - was going to add complexity and bugs I didn't want to deal with.

But if you remember, we don't actually have a way to declare higher order function types in Tangent. And since functions look like phrases, it's not quite clear how to allow users to both specify higher order functions and anonymous functions that fit that style of calling convention. What I settled on is a little bit weird, so bear with me.

To say that a parameter takes a higher order function, you build a small phrase in the parameter declaration. For example:

twofer(fn(int): void) => void {
  fn 2;
}

or

sort (collection: list int) with (compare (int) to (int): int) => list int { ... }

Since you can't ever access the delegate parameter directly, just specifying the type is sufficient. fn above compiles to a .NET Action<int>, with the phrase style calling, and compare (int) to (int) to a Func<int, int, int>, again allowing the function to call it with the right phrase pattern.

To call these functions, you just use lambda syntax similar to C#:

twofer (x) => {print x;};

or

sort stuff with (a) (b) => { a.Compare(b); };

Right now, the parens and curly braces are required. I expect the parens to become not required and the curly requirement likely to stay. Annoying, but this is better than many of my alternatives.

Now, there's a little bit of a trick to this. In C#, it is very clear what types the lambda's parameters are. But in Tangent, we infer the order of operations - both using the lambda as an argument, and in the body of the lambda itself. As the types of the lambda parameters change, the actual operations taken in the body of the lambda can also change.

What happens is that the entire lambda is considered a node in the expression. When the pattern twofer (Action<int>) sees the tokens "twofer" <lambda> it checks to see if the argument matches the parameter. Does it have the same number of parameters? If x is an int and we need to return void, does the lambda parse? If not, then the match fails and the compiler moves along, trying different interpretations of the statement.

This will allow some mildly weird behavior, like the body of lambdas effecting which overload of a function is used. I'll look to have an example of that once I'm more sure it works properly. Likewise, I want to make sure that my closure implementation actually is correct. It was a little too easy.

Wednesday, December 2, 2015

Cleanup

So, at the core of Tangent is the code that takes all of the phrases the user has defined and figures out how to grok that statement. In previous iterations of the language, that code has been hairy, nasty, nested cruft that has made it difficult to add features and debug things when they go south.

In this iteration, things were better. I was explicit in defining the various things that the language could define, and good about keeping things simple. Unfortunately, this led to a bit of duplication in this core code because the simplest way to deal with these definitions was just to have different handlers that did almost the same thing.

Not great. So I spent some time making some better abstractions and cleaning up that core code. In the end, it works akin to a shift reduce parser - the phrases define what pattern should be matched and depending on what is being declared (a parameter, a type, a function, etc.) it creates a different expression type in the abstract syntax tree. The new design better shares the matching and use of phrases, while leaving the "what happens when this is matched" variable. That allows better testing too, which is nice.

That should let me more easily move into local variables or delegates depending on what I feel like doing.