Hacker News new | ask | show | jobs
by pdpi 1107 days ago
It's easier to make the parallel between type-level and value-level reasoning.

1 is a value, and int is a concrete type.

function increment(x) { return x + 1 } is a value-level function. You feed it a value x and you get a value back. List<T> is a type-level function: you give it a concrete type T, you get another type back.

function applyTwice(f, x) { return f(f(x)) } is a higher-order function that takes a functions as an input. A higher-kinded type is a higher-order type function.

As a concrete example, consider this pseudo-Java method:

    List<B> map<A,B>(Function<A,B> fn, List<A> as) { ... }
You take a list, and you return a list. Thing is, Java has several list implementations: LinkedList, ArrayList, CopyOnWriteArrayList, and a few others. What I'd like to express is that whatever concrete list type goes in is also the concrete type that comes out. If java allowed it, you could express it like this:

    L<B> map<L<T> extends List<T>,A,B>(Function<A,B> fn, L<A> as) { ... }

This map is generic on L, A, and B, but also L is itself generic, so map is "twice-generic", if you will.
1 comments

Finally an explanation that makes sense. Thank you :)
In case you're wondering: Where this becomes incredibly powerful is in how it relates to ad-hoc polymorphism. Again, imagine this is pseudo-java.

    // Alternative one: subtype polymorphism. You have to implement Format as part of your type.
    interface Format { String format() }
    
    void printAll<Thing extends Format>(things List<Thing>) { things.forEach(thing => print(thing.format())) }

    // Alternative two: ad-hoc polymorphism. You can implement Format separately from the type you're implementing it for
    interface Format<T> { String format(T t) }
    
    void printAll<Thing>(things List<Thing>, Format<Thing> formatter) { things.forEach(thing => print(formatter.format(thing))) }
Doing ad-hoc polymorphism manually like this is obviously annoying as hell, it's the equivalent of doing dynamic dispatch in C by explicitly passing around vtables, but e.g. Haskell and Rust have direct support for ad-hoc polymorphism through typeclasses and traits respectively. Scala's implicits still require some amount of faffing about, but still make things much easier.

The bit where HKTs come in is when you want to have your interfaces talk about generics:

    // HKT goes here ----V
    interface Iterable<C<_>> { forEach<T>(C<T> collection, Consumer<T> fn) }
    
    
    void printAll<Thing>(things List<Thing>, Format<Thing> fmt, Iterable<List> iter) { iter.forEach(things, thing => print(fmt.format(thing))) }