I'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 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:
"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 ...
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.
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>`.
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
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.
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.
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.
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.
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?
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),
C<B> map(C<A> coll, Function<A, B> fun)
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.
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,
C<B> foo(Mappable<C> inst, C<A> coll)
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) {
var out = 0;
inst.map(coll, k -> {out++; return k});
return out;
}
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.
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.
esperent|2 years ago
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?
demurgos|2 years ago
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:
ReleaseCandidat|2 years ago
moomin|2 years ago
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.
stevefan1999|2 years ago
pdpi|2 years ago
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: This map is generic on L, A, and B, but also L is itself generic, so map is "twice-generic", if you will.ReleaseCandidat|2 years ago
Edit: I hope now all asterisks are properly escaped ...
DougBTX|2 years ago
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...
epolanski|2 years ago
https://gcanti.github.io/fp-ts/modules/HKT.ts.html
noobdev9000|2 years ago
mbwgh|2 years ago
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
ToJans|2 years ago
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.
arnejenssen|2 years ago
jdonaldson|2 years ago
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.
davedx|2 years ago
frogulis|2 years ago
In short I don't think so but I'd also love a good explanation as to why I'm wrong.
pyrale|2 years ago
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.
amitport|2 years ago
[Edit: why the down vote?]
classified|2 years ago
xupybd|2 years ago
xmcqdpt2|2 years ago
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, 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], 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.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.