Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

type-level function application #17961

Conversation

KiaraGrouwstra
Copy link
Contributor

@KiaraGrouwstra KiaraGrouwstra commented Aug 22, 2017

See the tests for usage. Syntax mirrors JS function calls, though expression-level variables must be lifted to the type level with typeof as before.

Progress toward fixing #6606 and others:

I took to heart the advice not to make the implementation too crazy -- although I added a type call node to parse this type variant of the traditional call expression, in the checker I opted to just 'cheat' and get it to evaluate Fn(A, B) as expression (null! as Fn)(null! as A, null! as B).

@Igorbek may now laugh at me for mocking null! as type notation.

Note: the ambiguous Ref<A>() is currently parsed such that the type argument list <A> belongs to the type reference, not to the function call. I've enabled Ref<><A>() as an alternative. Before, adding an empty type argument list <> to a non-generic type would error stating the type is not generic. Edit: I've removed type argument lists on type calls as they don't seem very useful.

@christyharagan
Copy link

christyharagan commented Aug 22, 2017

Apologises for the potentially silly question/observation, but getting rather scared by the number of changed files I noticed there's a lot of /**@class*/ comments added all over the place. Is this intentional?

@KiaraGrouwstra KiaraGrouwstra force-pushed the 6606-type-level-function-application branch from 6165eee to 080b391 Compare August 22, 2017 15:58
@KiaraGrouwstra
Copy link
Contributor Author

@christyharagan: No, I messed up my rebase, somehow it showed all changes on master since I branched off. Fixed it now.

@Igorbek
Copy link
Contributor

Igorbek commented Aug 22, 2017

@tycho01 I was also hacking around this feature a little bit some time ago, although with slightly different syntax (typeof (expr)).
The issue I discovered when tried to type your alternative to promised T that it is mess in overload resolution when generics types used, see #17471 (case 2, specifically)
Have you tried such cases already?

@atrauzzi
Copy link

@tycho01 - Wow! So is this going to be the one that lets me get past atrauzzi/protoculture-react#5? :)

@KiaraGrouwstra
Copy link
Contributor Author

@Igorbek: I added the current issues to my original post now -- your issue seems to relate to that overload selection thing, identified by @jcalz at #12424 (comment).

@atrauzzi: just tried, seems heterogeneous map isn't working yet. I'll check it out.

@KiaraGrouwstra KiaraGrouwstra changed the title add type-level function application WIP: add type-level function application Aug 22, 2017
@Igorbek
Copy link
Contributor

Igorbek commented Aug 22, 2017

BTW, I'm just curious about ambiguity when we mix expressions and types. What would be here?

type F = () => number;
const F: () => string;

type TypeOfF = typeof (F()); // is F expression or type?

@KiaraGrouwstra
Copy link
Contributor Author

@Igorbek: after changing const to let since it lacks an initializer:

  • typeof (F()): doesn't compile; typeof still just expects an entity name, while F() there would parse as a type.
  • typeof F(): string, using the expression variable courtesy of typeof
  • F(): number, type

@Igorbek
Copy link
Contributor

Igorbek commented Aug 23, 2017

Ah, I see. So the syntax is

type F = () => number;
const F: () => string;

type TypeOfF = F(); // number

So, you're not trying to use typeof at all, and only sticking with function call (with overloads) case.
Do you think it is still worth to have extended typeof that works with any expressions?

@KiaraGrouwstra
Copy link
Contributor Author

@Igorbek:

So, you're not trying to use typeof at all

Right. I'd talked a bit about my considerations on that in #6606 (comment) -- I just saw this as a simpler way to look at it than figuring out how functions/arguments stored in types would have cleanly fit into the earlier interpretations.

6606 evidently split into distinct proposals, and if people like both, then why not.
One pitfall to look out for there is the precedence you'd mentioned before; ambiguity would exist for both typeof fn[k] as well as typeof fn(v): "are we in expression land or in type land? Is that an expression variable or a type variable?". With post-fix operators like that the answer to that becomes a bit less obvious.

Earlier snippet on this from #6606 (comment):

I think see where the ideas clash -- current proposals have contradictory views on whether the typeof keyword would switch its expression to the value level, vs. (in my interpretation) leaving typeof as-is, but exposing function application syntax on the type level.

In my view, the contradiction is somewhat accidental though. I wouldn't disregard the legitimacy of either use-case; if both were to be implemented I could see an extra keyword avoiding semantics clash. I'm honestly indifferent whether keywords will end up one way or another -- I just wanna type shit.

By the way, thank you for your thread on the overload resolution at #17471 -- I fear the major use-cases of this PR will have to defer to that, as they mostly depend on that overload pattern matching...

Worse yet, despite most of the simple tests working, it looks like none of the useful applications of this are working yet. Function composition doesn't, heterogeneous map doesn't, overload selection doesn't... ouch.

KiaraGrouwstra added a commit to KiaraGrouwstra/TypeScript that referenced this pull request Aug 24, 2017
Fixes the third part of microsoft#5453, needed to effectively type `bind` (Function.prototype), `curry` and `compose`. Depends on microsoft#17961 type calls, microsoft#17884 type spread and microsoft#18004 tuple spread. The first 2/3 are included, so changes will look more cluttered here though new changes just span 10 lines over 2 functions. Will properly test this when those are ready -- now still broken.
@KiaraGrouwstra
Copy link
Contributor Author

Further debugged a bit, seems composition might be suffering from the same evaluation order issue that overloads are, that #17471 mentioned earlier. I still need to check what the problem for map is, but otherwise the issues here seem to come down to that one now.

@dead-claudia
Copy link

dead-claudia commented Mar 11, 2018

You may want to add #1213 to the list of things that could be closed as a result of this bug. (Instead of "higher order types", I pass generic type-level functions instead.) There are potential concerns about type inference (it requires generalization before substitution rather than the usual other way around), but I'm not sure if that 1. is a real issue, and 2. if it would block the closure of that bug.

@landonpoch
Copy link

I'm very excited to see the TypeScript interfaces created for fantasyland by @isiahmeadows based on these changes! I'm not sure I understand the "generalization before substitution" concern mentioned above. Also, maybe HKT get added to TS later. What I do know though, is that a lot of people don't have an opportunity to use this type of abstraction today because there aren't many strongly-typed languages that support it that are commonly used in larger orgs. I'd like to cheer on these efforts because I feel this is a great opportunity for a lot of professionals to really start to understand and use these powerful concepts. Typescript is a great platform for this because of its ubiquity. I'm continually impressed by how robust its type system is.

@dead-claudia
Copy link

@landonpoch

I'm not sure I understand the "generalization before substitution" concern mentioned above.

Normally, TypeScript is used to doing things like this:

interface Foo<T> { ... }

declare function foo<T>(value: T): Foo<T>

In this case, you can infer the generic type just by the value itself.

Now, consider this type:

interface Functor<T extends <A>(x: A) => Functor<T, A>, A> { ... }
declare function foo<T extends <A>(x: A) => Functor<T, A>, A>(value: T(A)): Functor<T, A>

In this case, you have to infer the generic type function from the value itself first, so you have to work backwards. (This is the [Gen] part of HM type inference, but TS doesn't use it.)

Here, it has to first infer value: T(A). We know that must be some abstract instance of Functor<T, A> due to the type signature's generic constraint on its return type. Now, we have to figure out A to figure out the second parameter. This can be deductively inferred through the type in a pretty straightforward fashion. Now, my question is whether you can infer T from A = ??? and T(A). You have to generalize here to infer T before taking the final substitution step to infer the structure (what TS cares about) of the return type Functor<T, A> (T is itself used within the implementation to get this everywhere).

Normally, this extra generalization step isn't required, but it could show up in other existing, more obscure scenarios, hence why I'm not sure if it'll be an issue:

interface Foo<T extends (x: any) => any, A extends (T extends (x: infer A) => any ? A : never)> { ... }
declare function foo<
    T extends (x: any) => any,
    A extends (T extends (x: infer A) => any ? A : never)
>(value: T): Foo<T, A>

I fixed an issue with my types where it wasn't carrying the type info correctly. Note that the fix requires you to always pass the concrete "nested" type around as well, thanks to the lack of simple existentials within interface types.

@KiaraGrouwstra
Copy link
Contributor Author

@isiahmeadows is the point here to cut down on boilerplate in e.g. gcanti's approach, or was that not working well yet?

That said, looks like your approach is using type call syntax for backward inference. I hadn't done anything for that. I'm not sure there's an easy way to do it though.

Like, for a given JS result value and input parameters, could you determine "the" function that would yield that result for those given values? I believe there would not be one correct answer. We're talking about the type equivalent of that scenario, but yeah.

@dead-claudia
Copy link

@tycho01

is the point here to cut down on boilerplate in e.g. gcanti's approach, or was that not working well yet?

Kind of, but that particular approach isn't correct with subtypes. Mine still requires you specify the type function, but it's correct provided all types are given (which was my goal).

That said, looks like your approach is using type call syntax for backward inference. I hadn't done anything for that. I'm not sure there's an easy way to do it though.

This specifically was my concern: I wasn't sure if the function could be inferred from its return value + arguments. It's possible to add it as a special case, but generalizing that is how you get (a subset of) inductive reasoning, and of course, we both know that's a hard problem. (I'm constantly impressed with what Haskell can infer.)

Like, for a given JS result value and input parameters, could you determine "the" function that would yield that result for those given values? I believe there would not be one correct answer. We're talking about the type equivalent of that scenario, but yeah.

Of course. But the easier problem to solve would be "what is the most general function that 1. matches the constraint and 2. yields that result for those given values", and that's the one users would expect. This conveniently happens to be the type specified in the constraint, for reasons I presume you're already aware of.

@jack-williams
Copy link
Collaborator

Here, it has to first infer value: T(A). We know that must be some abstract instance of Functor<T, A> due to the type signature's generic constraint on its return type.

I'm abit lost here. How do you know that the supplied value actually is an instance of functor? To check that the supplied value is assignable to Functor<T,A> to you not need to know T first, or A too?

@KiaraGrouwstra
Copy link
Contributor Author

@isiahmeadows @jack-williams:
Taking a step back to find a simpler solution, I think map implementations such as that in Ramda use dispatching to achieve delegation to datatype-specific implementations.
I think type calls (this/6606) could help to type those, to delegate Maybe-specific logic to a Maybe.prototype.map's typing instead. Sorry static-land.

@dead-claudia
Copy link

@jack-williams

I'm abit lost here. How do you know that the supplied value actually is an instance of functor? To check that the supplied value is assignable to Functor<T,A> to you not need to know T first, or A too?

You need to know A, too, but in this scenario of mine, we always know A (via the argument).

@dead-claudia
Copy link

@tycho01 I was slightly off about the inference rules - it only works when all invocations of T are structurally identical. In this example, we need a limited amount of induction to figure out const fb = fa.map(a => b), where fa is assignable to Functor<*, *>.

interface Functor<T extends <A>(x: A) => Functor<T, A>, A> {
    map<B>(this: T(A), f: (x: A) => B): T(B)
}

The inductive step here would be generating T from a known result. The least surprising one to infer is the constraint. Unless T's return type itself is a conditional type, we can only have one meaningful type to reify it as. The only source of complications is if T isn't known or inferrable before the function call and the generic constraint itself is not onto:

interface Foo<T> { ... }
type Mapper = <A>(x: A) => A extends {foo: any} ? Foo<A["foo"]> : Foo<A>
declare function choose<T extends Mapper, A, B>(foo: T(A), bar: T(B)): T(A | B)

In this scenario, given choose(foo, bar) and known types for foo and bar, we would have a base type to infer A and another to infer B, but we would need to infer the function. The least surprising T to infer here would just be the constraint itself.


Side note: a variant of Foo<any> where the parameter's type is inferred existentially from context, like Foo<*>, would make it a lot easier to specify well-typed return types.

@KiaraGrouwstra
Copy link
Contributor Author

@isiahmeadows: well, even if we'd figure out how to make it (any languages featuring reference implementations?), even the trivial empty tuple PR has been sitting stale here.
I'd consider giving Flow's $Call a go to see how far that gets for typing stuff.
On reference implementations and $Call, I did recently check $Call's implementation to compare, but it didn't seem similar somehow.

@dead-claudia
Copy link

dead-claudia commented Mar 29, 2018

@tycho01 I'm not sure whether I could do an equivalent typing in Flow, but I'll try once I have time again.

Edit: clarity

@KiaraGrouwstra
Copy link
Contributor Author

@isiahmeadows: seems Gulio tried HKTs and static land in Flow.
The coolest Flow type I've seen was this deepSeq one (pretty much R.path) by @goodmind.

@typescript-bot
Copy link
Collaborator

Thanks for your contribution. This PR has not been updated in a while and cannot be automatically merged at the time being. For housekeeping purposes we are closing stale PRs. If you'd still like to continue working on this PR, please leave a message and one of the maintainers can reopen it.

@dead-claudia
Copy link

@tycho01 What's the status of this?

@treybrisbane
Copy link

As much as I'd love to see this, it doesn't look likely to get approval from the team based on @mhegazy 's most recent comment on the matter: #6606 (comment)

@dead-claudia
Copy link

@treybrisbane I'm also coming under the impression that this isn't exactly the best way to handle higher-order types, particularly within TypeScript itself. It's hackish, and it doesn't fully address the issues it needs to solve.

@KiaraGrouwstra
Copy link
Contributor Author

KiaraGrouwstra commented Jul 29, 2018

@isiahmeadows: in another PR @sandersn explained to me the issues with AST rewrites.
I'd used those there and here to make for a simpler implementation -- just pretend we had an actual function call using the right types!

In this PR, those issues were pretty much on a different level: iirc when your type call would error, the error squiggles would show up in a different place (the 'function' rather than the call?).
The 0-safe divide idea turned out even more unstable.

I still believe a feature like this would be a break-through for the language.
Given the knowledge and support it'd require though, it'd pretty much need to come from within the walls of the core team itself.

it doesn't fully address the issues it needs to solve

Would you mind elaborating on that? Perhaps understanding this will help pave a way forward.

@dead-claudia
Copy link

@tycho01

Would you mind elaborating on that? Perhaps understanding this will help pave a way forward.

First, it requires inductive generalization, something TypeScript lacks altogether. Second, declarations and non-inferred usages for common types like Functor<...> and Monad<...> are very difficult - consider the excessive F-bounded polymorphic boilerplate my gist has, and that's probably the less boilerplatey of the two sides. There's other various usability issues, too, but these are the first two that come to mind.

IMHO something that integrates with polymorphic this within interfaces is ideal, since almost all use cases for higher-order types reduce to something that involves it. It's starting to make me consider this even more as a better solution to the problem.

@Meligy
Copy link

Meligy commented Jul 30, 2018

Are some of the original issues this work tries to achieve fixed by Tuples in rest parameters and spread expressions in v3?

@dead-claudia
Copy link

@Meligy Not really. This is addressing a completely independent set of issues, such as conditional types, higher order types, and mapping over tuples/etc. One of those has already been implemented separately, but using this for the second is a major hack that doesn't even fully work most of the time and the third can only be partially implemented via this + a bunch of arcane type magic, really.

@KiaraGrouwstra
Copy link
Contributor Author

@isiahmeadows: yeah, I think you're right about polymorphic this for handling ADTs -- guess these are separate issues.

[mapping over tuples/etc] can only be partially implemented via this + a bunch of arcane type magic, really

While my tuple PoC did use recursion, fwiw object map is trivial with this -- { [P in keyof T]: F(T[P]) }.

@dead-claudia
Copy link

@tycho01 But how well does it work with tuples?

@weswigham
Copy link
Member

@isiahmeadows We just merged a change that makes mapped types map over tuple elements.

@dead-claudia
Copy link

Okay, then never mind. Thanks! 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.