microsoft / typespec

https://typespec.io/
MIT License
4.51k stars 216 forks source link

Calling functions in template instantiations #521

Open nguerrera opened 2 years ago

nguerrera commented 2 years ago

See https://github.com/microsoft/cadl/issues/462

Here we have a problem that KeysOf and ParentKeysOf are distinct types that we process twice. It would be nice if we could write:

alias ParentKeysOf<T> = KeysOf<Parent<T>>

However, we don't have currently have a way to author Parent<T>. We have a JS function exported getParentResource but we don't have a way to call it. It would be nice if we somehow could.

Brainstormed options

1. Allow calling exported functions in type references.

alias ParentKeysOf<T> = KeysOf<getParentResource(T)>

This seems very powerful, but I have some concerns:

image

2. Allow calling exported functions using template instantiation syntax

alias ParentKeysOf<T> = KeysOf<getParentResource<T>>

Here the < > parses this as a template instantiation, and when we resolve getParentResource to a function, we call it with the type arguments, and cache it like any other template instantiation. This solves the caching, but not the completion noise.

3. Explicit template to function bridge

alias Parent<T> is getParentResource(T);

I struggled with the syntax here so consider this a straw-man. I want something that is different than alias =. I first had model is but realized it would be useful if this could return a non-model, and then realized it really is equivalent to a templated alias! But if we use alias = we are really back to (1). The idea here is that you can make a 1:1 between an alias and a function. So I landed at alias is but I don't like it.

But syntax aside this solves the intellisense pollution and allows for idiomatic type (Pascal cased, noun) and function (camel case, verb) names.

4. Explicit opt-in of functions as templates

export function $$Parent(program: Program, type: Type) {
   return getParentResource(program, type);
}

Here we borrow from decorators and have some way to opt a function into becoming a template on the Cadl side. Straw man: $ is a decorator $$ is a template. This also solves the intellisense pollution. It could further allow one fewer declaration if you imagine having getParentResource implementation inlined into $$Parent.

bterlson commented 2 years ago

I am worried about establishing yet another naming convention, in part because it makes things more confusing in projection-or-cadl-function land. We're potentially looking at 4 classes of functions IIUC:

  1. Decorators
  2. Templates (e.g. things which return structures) like Parent.
  3. Utility functions that may be called from CADL (e.g. things which return useful values) like getAddedOn.
  4. JS functions not intended to be called from CADL.

For syntax options for declaring 2 (and maybe 3) I think we should consider alignment with future function and decorator declaration syntax. E.g. I would expect something like this to work:

fn @parent(parentType) {
  self.state.set("parentType", parentType);
}

fn getParent(type) {
  return type.state.get("parentType");
}

alias Parent<T> = getParent(T);

(And maybe the alias form ensures instantiation caching happens on the cadl side? I dunno)

There are a couple more options we could consider (not mutually exclusive):

5. JS functions are exported only from package.json main, and not imported into cadlMain

It's up to the library author to split their implementation code such that they only export things into cadl that they intend to be called from cadl.

6. Use namespaces, e.g. require that cadl-callable things have a namespace

If a function doesn't have a namespace, it doesn't get bound. Not sure if it's ever good to have an un-namespaced function so this is maybe ok?

7. Bind into cadl based on signature

Any function which takes program (or later, context) as first parameter is bound in cadl. A quick-and-dirty regexp on the toString of the function may be sufficient here?

nguerrera commented 2 years ago

Great points.

I think requiring that Cadl callable things have a namespace is reasonable no matter what else we do. It seems like we would never recommend putting such things outside a Cadl namespace and unlike in .cadl, I have no real concern about adding one line of boilerplate to get started with JS interop.

I also think binding on signature is reasonable. I would really like to avoid regexes though. I think maybe it makes sense to go after signature help first so that we have more typing metadata available about functions and decorators. It's possible that with good typing we won't clutter tooling with option 1 (functions in type references) as we could only show functions that return a type in type reference completion. And then alias = as the way to cache the call and give it a nice type name seems fine. We can further use this typing to make it an error to call a function that doesn't return a type in a type reference.

So that's how I'm now leaning. It seems the most expressive. And maybe if we go after signatures first then it will be clearer if that's the right direction. I've been meaning to write up some options for decorator and function signatures since the dev doc proposal left that as a follow-up design discussion to be had. I'll try to have that up soon.

bterlson commented 2 years ago

Funny, I thought 5 was the simplest and might end up obviating other approaches.

nguerrera commented 2 years ago

Maybe I don't understand, but 5 seems like something that helps fix the completion cluttering, but isn't a full alternative. It still needs to be paired with something like 1, 2 or 3 so that I have a way to invoke the functions for these scenarios, right?

I may be over-reacting to the current mess of seeing all helper functions, but it still would seem valuable to me if we didn't offer to complete functions that don't return types in a context where only a type can go. That said, that is harder. Maybe we can try 1 + 5, and then use typing later on when it comes online for signature help to make completion smarter?

tjprescott commented 1 year ago

I kind of like option 2 combined with some of Brian's suggestions, but I was afraid this was "let's definite Typescript functions in cadl" which it's not, so I'm okay with any outcome. Definitely seems like this would be useful, and I agree that the cluttering of helper functions is very unfortunate.