Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

> Asked differently: If one were to design a programming language that only has basic types (primitives, structs/classes, ...) and templates to allow functions/classes to operate on any type (but not more than that; substitute template type with the actual type, compile this, nothing more), what feature will users of the language be missing and complain about?

This is a really good question. There are some languages that work as you describe: SML and some others in that family. There are generic functions and types, but the type parameters are basically just placeholders. You can't do anything with a value whose type is a type parameter, except store it and pass it to things that also take type parameters.

That gives you enough to write a nice reusable vector type. But it doesn't let you easily write a nice reusable hash table. You can, but users have to pass in an explicit hash function that accepts the key type every time they create a hash table.

It might be nice if a type itself could indicate whether it's hashable and, if so, what it's hash function is. Then, if you create a hash table with that key type, it automatically uses the hash function defined by that type.

Now you need some sort of constraints or type bounds in your generics. That's what traits in Rust and bounds in Java and C# give you. (The literature calls it "bounded quantification".) It's a big jump in complexity. But it does mean that now you can call functions/methods on arguments/receivers whose type is a type parameter, and those calls can be type checked.

Bounds are themselves types, so what kinds of types can you use in bounds? Can they be generic? If so, what kinds of type arguments are allowed? Can you use type parameters from the surrounding type?

For example, which of these are OK and which aren't (using Java-ish syntax):

    class A<T extends Foo> {}       // 1.
    class A<T extends Bar<Foo>> {}  // 2.
    class B<T extends B<Foo>> {}    // 3.
    class B<T extends Bar<T>> {}    // 4.
    class B<T extends B<T>> {}      // 5.
Any kind of bounded quantification will give you 1-3. What about 4 and 5? This is called "F-bounded quantification". Why would you want such a thing?

Collections with fast look-up are important, which is why we extended our generics to enable us to write nice reusable hash tables. But some data types aren't easily hashed but can be easily ordered. A sorted collection is faster than an unsorted one.

How would we write a generic sorted collection? We could require you to always explicitly pass in an ordering function for any given element type, but it would be nice if the element type itself could supply is order function.

You could define a "Comparable" interface that a type can implement to support comparing an instance against another object of some type, like:

    interface Comparable<T> {
      int compareTo(T other);
    }
And then implement it on your type, like:

    class Color implements Comparable<Color> {
      int r, g, b;

      int compareTo(Color other) => ...
    }
In our sorted collection, elements all have the same type, so the bound that we need looks like:

    class SortedCollection<T extends Comparable<T>> { ... }
Notice that we have "T" inside the bound. That's F-bounded quantification.

Using type parameters inside a bound isn't the only place recursive types like this show up. Let's say you wanted to make a generic type comparable. You'd do something like:

    class Pair<T> implements Comparable<Pair<T>> {
      T a, b;

      int compareTo(Pair<T> other) => ...
    }
Now here, the implements clause is using not just the type parameter of the enclosing type, but the entire type.

We had a couple of fairly modest goals:

* Be able to create reusable hash tables where the hash function is inferred from the key type.

* Be able to create reusable sorted collections where the comparison function is inferred from the element type.

And in order to get there, we needed generics, bounds, and even F-bounded quantification.

Adding even a little more usefulness to our collection types will quickly have us reaching for variance annotations, associated types, and even more exotic stuff.



> It might be nice if a type itself could indicate whether it's hashable and, if so, what it's hash function is. Then, if you create a hash table with that key type, it automatically uses the hash function defined by that type.

> Now you need some sort of constraints or type bounds in your generics. That's what traits in Rust and bounds in Java and C# give you.

Isn't having a function "Hash", called in your template, that takes your type as argument (and give compiler error if the function doesn't exist for this type, as a consequence of substituting in your type) sufficient for this?

In other words, duck typing


Yes, C++'s duck typing approach is one solution to this.

It takes the solution out of the type system, which keeps the type system simpler.

But it effectively turns your compiler into an interpreter, and an interpreter which may fail.

One way to think of C++'s notoriously huge, incomprehensible template compile time errors is that they are effectively stack traces of the template expansion interpreter running at compile time. When you see one of those errors, you have to figure out which chain of compile-time execution led to it.

Everything that's frustrating about dynamically typed errors that makes users reach for static types is exactly true of C++'s template system as well. (And, conversely, everything that's powerful and simple about dynamic types is true of C++'s template system.)

It's actually even worse in C++ because of SFINAE. The "interpreter" running at compile time in C++ doesn't just abort on the first error. It's like an interpreter for a dynamically typed language that also supports overloading. Any time a function has an error, the interpreter backtracks and tries the next overload. If all overloads fail, then it keeps unwinding.

So what you get isn't just a call stack on a template expansion error, it's a call tree of every place it tried to get to that failed.


This was a fantastic write up. Very interesting, thanks so much!




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: