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?
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`)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 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.
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}}}>>
They're fairly common in Scala too, and I believe in OCaml through modules.
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.Edit: I hope now all asterisks are properly escaped ...
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...
export type LinkedWorksheetsRecord = Record<WorksheetId, Record<WorksheetId, ReferenceTypeId[]>>;
export type LinkedWorksheetsMap = Map<WorksheetId, Map<WorksheetId, ReferenceTypeId[]>>;
What I would rather have written instead is however something like this: export type LinkedWorksheets<T> = T<WorksheetId, T<WorksheetId, ReferenceTypeId[]>>;
...
const myMap: LinkedWorksheets<Map> = ...;
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
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 not associated with effect-ts at all.
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.
In short I don't think so but I'd also love a good explanation as to why I'm wrong.
The Typescript type checker is (or at least was) already Turing-complete (https://github.com/microsoft/TypeScript/issues/14833) without fully supporting dependent types.
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.
[Edit: why the down vote?]
That's not the meaning of dependent types, and dependent type checkers don't require runtime information.
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,
interface Mappable<C> {
C<B> map(C<A> coll, Function<A,B> fun);
}
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.
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: `(* -> *) -> *`.