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

Provide way for a function to use the contextually expected type as its return type #386

Closed
sanjiva opened this issue Dec 12, 2019 · 9 comments
Assignees
Labels
enhancement Enhancement to language design lang Relates to the Ballerina language specification status/inprogress Fixes are in the process of being added

Comments

@sanjiva
Copy link
Contributor

sanjiva commented Dec 12, 2019

There are several place where a function needs to "data bind" its result. Two examples:

  1. SQL query result
  2. response from an HTTP request

We do the 1st one currently in a rather hacky way:

    table<DataResult> ret = dbClient->select(SELECT_RESULTS_DATA, DataResult);

(I'm ignoring some type checking rules here - that line does not compile.)

What we need is a way to say that the return type is the contextually expected type and to pass that typedesc into the function as a parameter so the code can try to return that. If there's a possibility that it might not be able to then the programmer can union the return type with error or possibly even panic.

Current http:get is as follows:

remote function get(@untainted string path, public RequestMessage message = ()) returns Response|ClientError 

What I want is something like this:

remote function get(@untainted string path, public RequestMessage message = (), @TypeParm typedesc retType = ?) returns Response|ClientError|retType

Then, I can just use it like this:

Person p = check client->get("/person/123");

This is using the @typeparam thing we're using in LangLib as a poor man's parametric typing system. (I'm probably not using it properly.)

[Summarized from an email discussion with James.]

@jclark
Copy link
Collaborator

jclark commented Feb 23, 2020

#426 can now do this.

@jclark jclark closed this as completed Feb 23, 2020
@jclark jclark reopened this Apr 21, 2020
@jclark
Copy link
Collaborator

jclark commented Apr 21, 2020

We will make #426 just cover the return type coming from a typedesc parameter.

Then we can cover defaulting the typedesc parameter to the contextually expected type here.

@jclark
Copy link
Collaborator

jclark commented Apr 21, 2020

This builds on #426 by allowed the parameter to be defaulted using the following syntax:

function query(string q, typedesc rowType = <>) returns stream<rowType, sql:Error> = external;

The <> is from how a cast sets the contextually expected type; the idea is that a cast that doesn't narrow the type gives you contextually expected type.

It's not quite as simple as using the contextually expected type. Consider

stream<Customer,sql:Error> stm = client->query(“SELECT ...”);

In this case, we want to default the rowType parameter to Customer. So the contextually expected type gives the return type and from this return type we determine the default for the rowType.

This needs the rework of contextually expected type from #392.

@jclark jclark self-assigned this Apr 21, 2020
@jclark jclark added the lang Relates to the Ballerina language specification label Apr 21, 2020
@jclark jclark added this to the 2020R2 milestone Apr 21, 2020
@jclark jclark added the status/pending Design is agreed and waiting to be added label Apr 21, 2020
@jclark jclark added the enhancement Enhancement to language design label Apr 21, 2020
@jclark jclark modified the milestones: 2020R3, 2020R2 Apr 24, 2020
jclark added a commit that referenced this issue Apr 28, 2020
Fixes #471.
Changing return type will be done in #426 and #386.
@jclark
Copy link
Collaborator

jclark commented May 16, 2020

I think we can deal with this independently of #392.

What we need to do is a simple form of unification.

Suppose we have a declaration of a function f with parameter t of type typedesc<T> and with return type R, which refers to t. Now suppose we have a call to f, with a contextually expected type of C and with no argument specified for t. What we need to do is unify C and R, where we treat R as having t as a variable. We will use the notation R{ t -> X } to mean the result of substituting X for t in R. Then what we need to do is find a type descriptor S such that R{ t -> S} is equivalent to C, where S must be a subtype of T. We then call f with a typedesc value representing S as the value for t.

In the above case, our function will be declared as:

function query(string q, typedesc<record{}> rowType = <>)
  returns stream<rowType, sql:Error> = external;

and we have a call:

stream<Customer,sql:Error> stm = query(“SELECT ...”);

So in this case. we are unifying stream<rowType, sql:Error> and stream<Customer,sql:Error> treating rowType as a variable, constrained to be a subtype of record{}. This is a super simple kind of unification. To unify these two, we recursively unify rowType with Customer, which we can do with a substitution of { rowType -> Customer }, and sql:Error with sql:Error, which succeeds without any substitutions. Thus the result of the unification is a substitution { rowType -> Customer }. We check that Customer is a subtype of record {}. Then we call call the query function with rowType parameter being a typedesc describing Customer.

@pubudu91
Copy link

pubudu91 commented Jul 20, 2020

A couple of questions:

  1. Should we allow inferring the typedesc param value when the contextually expected type is a union? If so, how would we go on about supporting it? The concern here is since the order in which the member types of a union doesn't matter, how do we go on about inferring a consistent value for the typedesc param? e.g.,
function getValue(typedesc<anydata> td = <>) returns Person|td = external;

// usage
Employee|Student|map<string> val = getValue();
  1. Is having multiple typedesc inferences ok? Something like the following for example,
function getTuple(typedesc<int|string> td1 = <>, typedesc<record {}> td2 = <>, typedesc<float|boolean> td3 = <>) returns [td1, td2, td3] = external;

// usage
[int, Person, float] tup = getTuple();

@jclark
Copy link
Collaborator

jclark commented Jul 20, 2020

On point 1, I think we need to support:

function getValue(typedesc<record{}> td = <>) returns td? = external;
map<string>? val = getValue();

Similarly for error. So if we are trying to infer t and we have a type T|t, I would suggest we require that the possible basic types for T and t are disjoint (e.g. in this case nil and mapping). (We do something similar with the applicable contextually expected type already.)

On point 2, I think it makes sense but I can't think of a case where we need it. If it's no extra work to implement, then go for it, otherwise I wouldn't bother at this point.

WDYT?

@pubudu91
Copy link

IIUC, then the type inferred for t would be the set of types not in the intersection of T and the contextually expected type right? For example, consider something like the following where E1 and E2 are error types and Foo and Bar are unrelated types.

function getValue(typedesc<any|error> t = <>) returns t|E1|E2 = external;

// usage
Foo|Bar|error val = getValue();

Then, the type inferred for t would be Foo|Bar? And it's the same inferred type for t in the following case as well?

function getValue(typedesc<any|error> t = <>) returns t|error = external;

// usage
Foo|Bar|E1|E2 val = getValue();

And regarding the support for multiple inferences, it's already supported in the current implementation I'm working on since it was straight forward with the work done for #426. But for unions, with the above complications, I think we'll have to allow just one inference.

@jclark
Copy link
Collaborator

jclark commented Nov 27, 2020

@pubudu91 Sorry I didn't reply earlier.

I allowed only one <> for now.

Your 2nd example isn't right because I cannot assign Foo|Bar|error to Foo|Bar|E1E2.

In your first example, I would expect t to be inferred as Foo|Bar, but I need to fix the spec to make that so. My point about disjoint was that if you have

function(typedesc<T> t = <>) returns t|R = external;

then T and R should have disjoint basic types. So this:

function getValue(typedesc<any|error> t = <>) returns t|E1|E2 = external;

wouldn't be allowed (assuming E1 and E2 are subtypes of error), but this would:

function getValue(typedesc<any> t = <>) returns t|E1|E2 = external;

Does that make sense?

@jclark jclark reopened this Nov 27, 2020
@jclark jclark added implementation/inprogress Implementation inprogress status/inprogress Fixes are in the process of being added and removed implementation/inprogress Implementation inprogress labels Nov 27, 2020
@pubudu91
Copy link

pubudu91 commented Dec 7, 2020

Ah yes, that makes sense. Thanks! Misunderstood what you said about disjoint sets earlier.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Enhancement to language design lang Relates to the Ballerina language specification status/inprogress Fixes are in the process of being added
Projects
None yet
Development

No branches or pull requests

4 participants