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

Calling functions in template instantiations #521

Open
nguerrera opened this issue May 9, 2022 · 5 comments
Open

Calling functions in template instantiations #521

nguerrera opened this issue May 9, 2022 · 5 comments
Labels
design:needed A design request has been raised that needs a proposal
Milestone

Comments

@nguerrera
Copy link
Contributor

See #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:

  • Tooling: I once had a bug where I offered all exported functions in type reference completion and it got quite cluttered:

image

  • Caching: I think it would be nice if we could cache these calls in the same way that template instantiations are cached so that you can do complex computation only once for a given set of type arguments.

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.

@nguerrera nguerrera added the design:needed A design request has been raised that needs a proposal label May 9, 2022
@ghost ghost added the Needs Triage label May 9, 2022
@bterlson
Copy link
Member

bterlson commented May 9, 2022

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?

@markcowl markcowl added 📌 and removed Needs Triage labels May 9, 2022
@nguerrera
Copy link
Contributor Author

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
Copy link
Member

bterlson commented May 10, 2022

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

@nguerrera
Copy link
Contributor Author

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
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
design:needed A design request has been raised that needs a proposal
Projects
None yet
Development

No branches or pull requests

4 participants