Free-types: Higher kinded types in TypeScript
github.comI'm reasonably proficient in Typescript although I wouldn't call myself an expert in type systems. But I'm not a beginner either.
However, I read though the readme and I have no idea what the usefulness of this is. Can anyone explain, in simple terms, some practical use cases for this?
A situation where I needed this were typings for Cycle.js a couple years ago. At this time, you could configure it to use the observable library of your choice but it was hard to type. In particular the observable could be a `Stream from "xstream"` or `Observable from "rxjs"`
This lib has a method `select` to return an observable list of DOM elements for a given CSS selector. What should be its signature? If using `xstream`, it should be `select(selector: string): Stream<HTMLElement[]>`, and if using `rxjs` it should be `select(selector: string): Observable<HTMLElement[]>`. Let's also assume that it has a second method `windowHeight` returning an observable for the window height (`Stream<number>` or `Observable<number>`).
We can make the lib object generic over the observable implementation, but you need HKTs to type it properly. The reason is that the lib is generic over an already generic type.
Here is an example:
// Without HKT (regular generic) // Problem: both `select` and `windowHeight` return the same type (we lose the HTMLElement[]/number information) interface Cycle<Obs> { select(selector: string): Obs; windowHeight(): Obs; } // The best we can do is type it as `Cycle<Stream<unknown>>` or `Cycle<rxjs.Observable<unknown>>`. // With HKTs, using syntax from this lib, you could do: interface Cycle<$HktObs> { select(selector: string): apply<$HktObs, HTMLElement[]>; windowHeight(): apply<$HktObs, number>; } // This allows to get the precise signatures we wanted (with the right observable impl, without `unknown`)Higher (-order) kinded types describe functions on type constructors. Type constructors are "generic types", like `T[]` that generate a list from a single type `T` or like `Map<S, T>` that generates a new type from two types `S` and `T`. A higher kinded type constructor is one, where you could use any (well, with certain prerequisites) of these type constructors to generate a new type. Let's say you want to have a function `map` (like `Array.map`) that works not only with `T[]` but also with `Tree<T>`, `Maybe<T>` (`= T | undefined`), ... HKT give you the possibility to write the type of such type functions.
The thing is, it takes a bit of experience to appreciate why HKT are important, and typically you can only get this experience using Haskell.
There’s a couple of ways to think about it: it gives you a way to talk about List rather than List of T, it enables you to write partial types like partially-applied functions, or it makes it possible to define Monads.
But as I say, none of these things will sound immediately useful unless you have experience of using those concepts already.
I think there is a certain kind of programmer who enjoys the aesthetics of higher-kinded types, and after having made the investment to truly grok them, wants these HKTs to also be useful in practice.
I don’t think the benefit ever materializes and highly abstract code is just indulgence.
Much like the people who endlessly tinker with their IDE/emacs/desktop environment/shell in the name of productivity.
Honestly, they are useful in practice, as long as you’re using a language that supports them properly (I’m not convinced TS is that language). Even without, they constitute a pattern that is very simple to implement which is very powerful.
People believed (and some still do believe) the same thing about generics.
true
Other than Monads, HKT can be used to easily write type-level functional programs [1]. This can for example help writing type-level parsers for other lanugages.
A real world use-case could be parsing GraphQL raw string queries and automatically infer the returned types based on a common schema, without using special code-generators. For instance you can come up with some magic function `gql_parsed` like:
doc = gql_parsed`query GetUser { user { name }}`
where doc is inferred as something like Doc<Query<{GetUser:{user:{name:string}}}>>
> The thing is, it takes a bit of experience to appreciate why HKT are important, and typically you can only get this experience using Haskell.
They're fairly common in Scala too, and I believe in OCaml through modules.
And C++ if you squint hard enough.
and F# with computations Seems common with functional programming, that it takes a mind-set change before seeing the need.
I wonder if there is any relationship between HKT and C#/Rust generics, from my perspective I always see HKT as "A type that accepts types that generates another type" and generic as "A functor that accepts types that generates another type". That makes me wonder if types and functors are exchangable.
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:
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:List<B> map<A,B>(Function<A,B> fn, List<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.L<B> map<L<T> extends List<T>,A,B>(Function<A,B> fn, L<A> as) { ... }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.
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.// 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))) }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))) }
"A type that accepts types that generates another type" is called a type constructor. And a "kind" is the type of a type constructor, just to have a new name and not needing to call that "type" too. "Generic types" like `Vec<T>` (in Rust) are type constructors, they "generate" a type `Vec<T>` from the type `T`. Type constructors that take one type as argument have the kind `* -> *` (or as Rust-y notation `fn(*) -> *` - read each asterisk as "type". Type constructor that take two types arguments have the kind `* -> * -> *` (Rust-y: `fn(*, *) -> *`). A higher (-order) kinded type takes (for example) a type constructor (here a type constructor which takes just one argument) and generates a type from it: `(* -> *) -> *` (Rust-y: `fn(fn(*) -> *) -> *`.
Edit: I hope now all asterisks are properly escaped ...
Edit2: oh, I guess misunderstood "A type that accepts types that generates another type". If you meant to say "A type that accepts (types that generates another type)", so a type constructor that accepts type constructors and not "(A type that accepts types) that generates another type", which is a type constructor.
Yeah, I think "types that generateS" is either a typo or sloppy English; it should be "types that generate".
I often wonder why it's so common for programmers with an acute awareness and mastery of syntax and grammar in programming languages to just throw all that precision and attention to detail out the window when it comes to natural language.
For Rust, pre-GAT, there was no way to “output” a type which could be “called” with further arguments (very fuzzy terminology, sorry, best I can do!) or maybe in other words, you could write functions which retuned values, but not new functions.
Nowadays, GATs support a bigger subset of HKTs, but still not everything as I understand it.
https://blog.rust-lang.org/2022/10/28/gats-stabilization.htm...
I wonder how this differs from HKT's implementation in fp-ts 2 and Effect-TS.
Why
A simple example I could recall from the other day is something like this:
What I would rather have written instead is however something like this:export type LinkedWorksheetsRecord = Record<WorksheetId, Record<WorksheetId, ReferenceTypeId[]>>; export type LinkedWorksheetsMap = Map<WorksheetId, Map<WorksheetId, ReferenceTypeId[]>>;
This is however not possible, because `Map` is a type constructor which expects two more type arguments `K, V` until it is a fully applied, concrete type `Map<K, V>`.export type LinkedWorksheets<T> = T<WorksheetId, T<WorksheetId, ReferenceTypeId[]>>; ... const myMap: LinkedWorksheets<Map> = ...;With a library like this, this is probably possible (unless I've missed something which wouldn't surprise me). It would unfortunately surely be more verbose.
Still, I would be against pulling in a dependency only for something like this. The above example is simple I believe, but not exactly a "killer-app". And no, Monads aren't either (if you don't limit effects and don't have do-notation) :P
Not really possible, because Record and Map aren't compatible at all. At best they both have something like `toString`. You'll need to define at least something like RecordFunctor<T> and MapFunctor<T> to make this useful.
Only if you want to abstract over them at usage-site.
In my case I only ever used the concrete types and converted between them at some point.
Oh, so it's just a type alias for readability. Then it makes sense.
Thank you for the explanation, seeing a simple concrete example really helps.
Looking forward to a proper monad lib in Typescript!
However, I can only assume that molding/abusing types like this might have a big - if not huge - impact on compilation times...
I've created a template-like generic type that allows you compose multiple kinds and replace any property of an object with a function returning the same type as the property, and vscode has such a hard time inferring types that intellisense has become unusable in this context.
Curious to see how this will turn out.
I'm using fp-ts https://gcanti.github.io/fp-ts/
fp-ts is great and still has active development but it has also been folded into effect-ts (https://github.com/Effect-TS/) which is based on Scala ZIO. I think long-term it will have a larger ecosystem, more active development and a better DX. The docs are far from complete, but give a good intro (https://effect.website/docs/getting-started).
I'm not associated with effect-ts at all.
Great tip; thank you!
Higher kinded types always remind me of regex. People think “I know, I’ll use HKT to solve this problem”. Now they have two problems.
These techniques can be way overkill for someone until they are dealing with an overwhelming amount of types to unify. It’ll seem like a terrible idea until that obstacle is encountered.
I’d love to be able to do dependent types in TS. Does this make that possible?
Might be wrong here, but I'm of the understanding that a dependent type system is undecidable, and so to have static dependent types you need to have a more restricted language, like the inability to write arbitrarily recursive functions.
In short I don't think so but I'd also love a good explanation as to why I'm wrong.
A fully-featured dependent type system may be undecidable, but that doesn't mean you can't make one - it just means that there will be valid programs that the type checker nevertheless rejects, or there will be valid programs for which the type checker never terminates. It doesn't stop you from creating a type checker in the first place; it just weakens the guarantees you can make about that type checker.
The Typescript type checker is (or at least was) already Turing-complete (https://github.com/microsoft/TypeScript/issues/14833) without fully supporting dependent types.
Typescript's type system is already undecidable (except that they limit recursion depth). I don't know much about dependent types but I'd guess it similarly doesn't matter much in practice that in the general case they're undecidable?
I guess you could use it to implement an algebra that allows you to build dependent types, but that would be unfit for practical uses.
As a case in point, Haskell has first-class experience for HKTs, and dependent types implementation in haskell is getting hindered by the limits of the language.
For anybody interested in the details, here is the last report of the ongoing implementation of dependent types in GHC: https://discourse.haskell.org/t/ghc-dh-weekly-update-6-2023-...
No. Typescript cannot access runtime values (I assume you mean types that depend on runtime values). In TypeScript types can depend on other types and it does support literal types which covers a lot of use cases. What do you need dependent types for?
[Edit: why the down vote?]
> No. Typescript cannot access runtime values (I assume you mean types that depend on runtime values).
That's not the meaning of dependent types, and dependent type checkers don't require runtime information.
"a dependent function may depend on the value (not just type) of one of its arguments" from wikipedia https://en.wikipedia.org/wiki/Dependent_type
The value does not exist during compilation. AFAIKT dependent type are used mostly during executable proof checkers to verify claims on the value-dependent types. So maybe using the term runtime is a bit to specific, but you do not have values (except literals) during typescript execution phase.
Well, I can't be bothered to edit wikipedia, but it's a confusing claim. Dependent typing does not require runtime information, results can be produced at compile time. The dependent type depends on a value, but the interesting property of the value is independently tracked in the type system, it's not retrieved from runtime information.
The gist of how it works is so:
In this example, you don't need to know runtime values, you need to know that you can safely get the first element. For that, you just need to know that something has been pushed at least once in the list.createEmptyIntList() => List<size=0, content=int> {...} push(List<size=a, content=b>) => List<size=a+1, content=b> {...} firstElement(List<size=a, content=b> | a > 0) => b {...}Since push returns a type that is different from createEmptyList, your typesystem has this information.
> The value does not exist during compilation.
type Z = []["length"] type One = [0]["length"] type Two = [0,0]["length"]The "value" in this case is symbolic, sort of like defining an array with variable length arr[x] and having the compiler verify that arr[x+5] is always out of bounds before knowing the actual value of x. If the type system is not powerful to prove correctness of some expression, you will need to insert a runtime check that lets the compiler to trust the value at compile time.
You still need a way of normalizing expressions that is consistent with your runtime. To say that types ((x: bool) => F<true && x>) and ((x: bool) => F<false || x>) are the same, wrt judgemental equality, requires normalizing both to ((x: bool) => F<x>).
I can't make sense of the examples. What would I use this for?
Are these like type classes?
They are related, but different concepts. An HKT is the type of a type constructor. An example described in another comment in this thread is if you want to define the map function on any collection C (in pseudo-Java),
Mapping over an Array would return an Array while mapping over a List would return a List. C<_> here is an HKT, the type of a type constructor with one argument.C<B> map(C<A> coll, Function<A, B> fun)In OOP a class is a description of what an object can do, often with a constructor that produces instances of the class with values as arguments. A type class is the same thing, but the constructor of a type class takes types as arguments. Type classes are useful because they allow defining functions for many types that share something without having to make the types inheritors of a common class (composition over inheritance basically).
For the example above I could define (pseudo Java) a type class like this,
and then if I want to define a function generic over anything that has a map I can do so by requesting both the thing and a Mappable of the thing,interface Mappable<C> { C<B> map(C<A> coll, Function<A,B> fun); }
Then I can use the Mappable instance to call map over any coll instances. For example a generic "size" function could be defined like this [1],C<B> foo(Mappable<C> inst, C<A> coll)
So that function would be enough to prove that Mappable<C> implies that C has a size and you can then define a function that gives you Sizeable from Mappable, which is useful composition.C<B> foo(Mappable<C> inst, C<A> coll) { var out = 0; inst.map(coll, k -> {out++; return k}); return out; }The example above is very boilerplate heavy because Java doesn't really support type classes but in Scala and especially Haskell the syntax is a lot cleaner.
[1] Usually you would use a fold here instead of a side effecting map.
> An HKT is the type of a type constructor.
That is a ("normal") kind `* -> *`. A Java `List<_>` or `Set<T>` would be a "normal/concrete" type constructor. In your example the problem is that `C` is a "generic" type constructor, so it has a higher (-order) kind, that takes a type constructor as argument (like `List<_>` or `Set<_>`) and constructs a type from this: `(* -> *) -> *`.
Yes, thanks for the clarification!