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

rfc: unphased functions #1711

Closed
wants to merge 17 commits into from
111 changes: 109 additions & 2 deletions docs/contributing/999-rfcs/2023-06-12-language-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,7 @@ For example (continuing the `Bucket` example above):

```ts
let bucket = new Bucket();

// OK! We are calling a preflight method from a preflight context
bucket.allowPublicAccess();
// ERROR: cannot call inflight methods from preflight context
Expand All @@ -776,15 +777,121 @@ let handler2 = inflight() => {
}
```

Bridge between preflight and inflight is crossed with the help of immutable data
structures, "structs" (user definable and `Struct`), and the capture mechanism.
The bridge between preflight and inflight is crossed with the help of immutable data
structures, user-defined structs, resources, and the capture mechanism.

Preflight class methods and initializers can receive an inflight function as an argument. This
enables preflight classes to define code that will be executed on a cloud compute platform such as
lambda functions, docker, virtual machines etc.

[`▲ top`][top]

#### 1.3.1 Phase-independent code

> **Note**: Phase-independent functions are not yet implemented. Subscribe to [issue #435](https://github.com/winglang/wing/issues/435) for updates.

Code that is not dependent on the phase of execution can be designated as phase-independent using the `unphased` modifier.

Using this modifier means that the function can be called from either preflight or inflight contexts.

```TS
let odd_numbers = unphased (arr: Array<num>): Array<num> => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the suggestion but I prefer inflight? instead of introducing another keyword to the language.

I think it reads nicely:

let foo = inflight? () => {
  return 34;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With just inflight, preflight, unphased perspective I think it reads nice, but my concern here is the first thing I think of when I see inflight? is an optional inflight.

(Thinking outloud)
The RFC mentions:

However, a preflight or inflight function cannot be passed to a function that expects an unphased function.

How would an expected unphased function look in a method signature if we use inflight?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little worried there could be a confusion or mental overloading with "?" since "foo?" looks a lot like an optional type. (It's also handy that "unphased" is fewer characters). But I'm not sure - let's keep the option open, it's probably the easiest part of the design to change 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that “optional inflight” is actually conveying the idea of “unphased” pretty well.

It implies that this is a preflight function that can also be used inflight.

We should be very careful to add additional keywords to the language. Every new keyword is a huge cognitive overload.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that thinking of the function as "optionally inflight" means it could be inflight or preflight.

However, we define optionality in the language as potentially having a "lack of value" so T? is [T || nil], which makes inflight? being [inflight || preflight] weird in my opinion.

let result = MutArray<num>[];
for num in nums {
if num % 2 == 1 {
result.push(num);
}
}
return result.copy();
};

// OK! We are calling an unphased function from a preflight context
let odds = odd_numbers([1, 2, 3]);

let handler = inflight () => {
// OK! We are calling an unphased function from an inflight context
let big_odds = odd_numbers([7, 8, 9]);
}
```

Phase-independent functions are useful for code that is useful across both
execution phases, such as for data manipulation, utility functions, etc.

Since phase-independent functions can be used inflight, they inherit the same restrictions as inflight functions, like not being able to call preflight functions or instantiate preflight classes.

But phase-independent functions can also be used preflight, so they inherit the same restrictions as preflight functions, like not being able to call inflight functions or instantiate inflight classes.

Phase-independent methods can be defined on resources:

```TS
class AwsBucket {
name: str; // preflight field

new() {
this.name = "my-bucket";
}

unphased object_url(key: str): str {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Big fan of the keyword approach over the previous ?, it feels very clear and is consistent with existing keyword modifiers.

// This method references a preflight field (this.name) -- that is
// allowed in both phases so it is OK!
return `s3://${this.name}/${key}`;
}
}
```

Phase-independent methods take on the additional restriction that they
cannot mutate fields of the resource. For example, the following is disallowed:

```TS
resource Bucket {
Chriscbr marked this conversation as resolved.
Show resolved Hide resolved
name: str; // preflight field

init() {
// initialize `name`
}

unphased set_name(name: str): void {
// ERROR: cannot mutate a preflight field from a phase-independent context
this.name = name;
}
}
```

An unphased function can be passed to a function that expects a preflight function or an inflight function. In this way, we can say that an unphased functions are a superset of both preflight and inflight functions.

However, a preflight or inflight function cannot be passed to a function that expects an unphased function.
An exception to this rule is that if a function is unphased, then we can automatically assume any functions passed to it or returned by it have a matching phase.

You can imagine that when a function is unphased, then "preflight" and "inflight" versions of it are generated at compile-time, and unphased-function types in parameters or return types are automatically converted to the appropriate phase.

For example, `Array<T>.map` is modeled like the following pseudocode:

```js
native class Array<T> {
Chriscbr marked this conversation as resolved.
Show resolved Hide resolved
unphased map<U>(f: unphased (T) => U): Array<U> {
// ...
}
}
```

At compile-time, since the function is unphased, preflight and inflight versions are generated:

```js
native class Array<T> {
preflight map<U>(f: preflight (T) => U): Array<U> {
// ...
}
inflight map<U>(f: inflight (T) => U): Array<U> {
// ...
}
}
```

Notice how "f" is automatically converted to the appropriate phase. This is possible because the function is unphased.
This way, when you call `Array<T>.map` with in preflight, it's possible to pass a preflight function to it, and when you call it in inflight, it's possible to pass an inflight function to it. (If you're calling `Array<T>.map` within another unphased function, then the unphased version of `Array<T>.map` is used.)

[`▲ top`][top]

---

### 1.4 Storage Modifiers
Expand Down
Loading