🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

Tangent: Typeclass preview

Published November 17, 2008
Advertisement
Got to work over the weekend to babysit and tweak some stuff regarding the release of the big project I was leading. No time for work on Tangent, but hopefully the release will free up my motivation and time to get back to it.

And now that at least the basics of generics are declarable (and making them usable is a short step down the road) I need to start thinking about making constraints better. Or more practically, how to solve the 'numeric' problem with C#.

During my original design, I thought about allowing free functions to be declared from within a class. That would keep the tidbits alongside the class, would behave a lot like operator overloading does now; and most relevant to this discussion was the ability to declare them abstract. They'd then act as a typeclass requirement that would let the programmer know that the operator is (or must be) supported, but isn't done there.

But I'm leaning away from that now. It doesn't solve too much; rather it has the same problems with preference that operator overloading does, would require a lot of work to interpret position and extrapolate that to usage. And in the end it seems like it'd involve too many special cases, too many exceptions, and it'd still be limited. It would be easier I think to work on the visibility stuff so that free functions can be declared alongside the class but still have access to internals if needs be.

Instead I'm looking more towards inferred constraints on the method. No need to extrapolate what's good, and if the method uses the form declared within the class. Here the method would essentially defer compilation until bind-time. If the compilation of a statement fails with that particular type, it can dump an error saying 'hey this statement doesn't work with type X'. That'll usually happen at compile time unless some reflection business is going on. Great.

This is particularly great for lambdas. It'll allow you to omit the parameter types like C# users will expect, and keep static typing goodness. For more lengthy or traditional methods though, it'll kinda suck. The parameters will exist as only identifiers, which is terrible for readability. Goes directly against a lot of the other language tendencies. Especially for well known typeclasses like 'numeric', supplying an interface type is super helpful so the programmer doesn't have to hunt through a method body to see what needs to be supplied.


So my first thought was to take the inferred constraint idea and move it to classes. Programmers could make a method that caused the requirements to be shown:

class Numeric{    require{        local T a,b,c;        c = a+b;        c = a-b;        c = a*b;        c = a/b;        //...    }}


Rather quickly I realized that such a construct is inviable. What compiles within this scope does not necessarily compile in a different scope. It can't follow the usual inheritance rules since stuff that returns a T might not return a T+U.

So what if we didn't allow T as an lvalue, and require the same sort of scoping... Yet more exceptions and special cases.


But what if I made it more like the usage constraints? They don't suffer from scoping issues, because they're specific to the method in question. They don't suffer from the inheritance problem because they're not inherited; more accurately they don't suffer the problem because all of the unification is done before doing the capability check.

So what the tentative plan is now is to use a similar construct, but to make it (mostly) distinct from classes. So, a type class can declare a block of statements that the type must correctly type-check for in addition to the usual elements of a class. The usual elements behave similarly to C# constraints or Tangent interfaces. Type classes may inherit from other type classes. The usual elements follow the same behavior; the requirements exist concurrently. To satisfy the interface, a type must satisfy all of them. When checked, the statements use the scope of their use, not the scope of their declaration.

or (with use of some psuedo-code for the typeclass and the includes):
// scope Apublic type class Printable{    require(T x){        print x;             // compiles fine. We don't care what print is here.    }}// scope Binclude scope A;public class Pirate{   public string Name = "Blackbeard";}public void show(Printable target){    print target;     // Compiles nicely, print (local T _) is on the list                      //  of things guaranteed by the interface.}show(new Pirate);     // Compile error, print  does not type check.                      //  thus the param doesn't satisfy the interface.// scope Cinclude scope A;include scope B;public void print(Pirate target){    Console.WriteLine(target.Name);}        public void ConsoleDump(Printable target){    print target;}ConsoleDump(new Pirate);  // great. print  type-checks at this scope.



Seems like a better way to provide a 'Numeric' interface and similar constructs. It also seems like it'd be kosher and unambiguous. That said, I'm probably going to take a good hard look into how Haskell and Scala deal with this sort of thing at the very least.

Feedback, links, suggestions, problems?
Previous Entry Umm?
Next Entry I am dumb.
0 likes 0 comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement