ballerina-platform / ballerina-spec

Ballerina Language and Platform Specifications
Other
168 stars 53 forks source link

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

Closed sanjiva closed 3 years ago

sanjiva commented 4 years ago

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 commented 4 years ago

426 can now do this.

jclark commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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\ 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 commented 4 years ago

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();

2) 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 commented 4 years ago

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 commented 4 years ago

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 commented 3 years ago

@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?

pubudu91 commented 3 years ago

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