-
Notifications
You must be signed in to change notification settings - Fork 196
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
Algebraic data types / union-like classes #977
Comments
I like the idea. What about adding names to the values, to keep the symmetry between enums and functions/classes? So something like: enum ArithmeticExpression {
Number(value: num)
Addition(augend: ArithmeticExpression, addend: ArithmeticExpression)
Multiplication(multiplicand: ArithmeticExpression, multiplier: ArithmeticExpression)
} Thoughts on this? |
I love this! Is there a way to map back and forth JSII/CDK's existing enum-like (aka union-like) classes to these enums? |
This is a very cool addition to the language and will give it more power. |
Two notes:
// JavaScript
class Frequency {
private constructor(discriminant, value) {
this.discriminant = discriminant;
this.value = value;
}
public static Cron(field1 /*string*/) {
return new Frequency(0, field1);
}
public static Rate(field2 /*Duration*/) {
return new Frequency(1, field2);
}
} |
I think the idea of adding names to values could be nice, but I also hear Yoav's point. I sense an equivalence between: enum ArithmeticExpression {
Number(value: num)
Addition(augend: ArithmeticExpression, addend: ArithmeticExpression)
Multiplication(multiplicand: ArithmeticExpression, multiplier: ArithmeticExpression)
} and: struct NumberValue {
value: num
}
struct AdditionValue {
augment: ArithmeticExpression;
addend: ArithmeticExpression;
}
struct MultiplicationValue {
multiplicand: ArithmeticExpression;
multiplier: ArithmeticExpression;
}
enum ArithmeticExpression {
Number(NumberValue)
Addition(AdditionValue)
Multiplication(MultiplicationValue)
} I think if there are a lot of values a user wants to add, then the minimal syntax (without labels) might encourage folks to just create their own struct type, which can be used outside of the context of the enum:
Also @yoav-steinberg good suggestion for simplifying the implementation 👍 |
It's possible there's some way to provide a partial mapping (at least for "creating" variants of union-like classes) -- the issue I see is that a lot of existing union-like classes in the CDK don't offer a way to discriminate between variants. For example, given an instance of In other words, the CDK uses union-like classes often as a way of combining data and behavior. But I think it might be cleaner/healthier if we define a union-like feature in Wing as purely a data type, and reserve "combining behavior and data" to resources and classes. |
I don't love the What about adding the concept of Example: sealed class ArithmeticExpression {}
struct NumberExpression extends ArithmeticExpression {
value: num;
}
struct AdditionExpression extends ArithmeticExpression {
augment: ArithmeticExpression;
addend: ArithmeticExpression;
}
struct MultiplicationExpression extends ArithmeticExpression {
multiplicand: ArithmeticExpression;
multiplier: ArithmeticExpression;
} Thoughts on this? |
Interesting - TIL about sealed classes. 🙂 I'm slightly biased towards the five-line version of |
Note that you can decide to forbid that by making |
Hi, This issue hasn't seen activity in 60 days. Therefore, we are marking this issue as stale for now. It will be closed after 7 days. |
Keep |
Hi, This issue hasn't seen activity in 60 days. Therefore, we are marking this issue as stale for now. It will be closed after 7 days. |
After some discussion with @staycoolcall911 - thought I'd write up a short motivation for the issue since ADTs might be unfamiliar to some folks, and the Wikipedia article isn't necessarily the best intro. The utility of ADTs is tied to a useful principle for avoiding a broad class of software bugs, which is to make invalid states unrepresentable. Suppose I want to represent the state of a network operation. Let's say that the state can either be "loading", "failure", or "success". If it failed, there will be an error code associated with it, and if it succeeded, there will be a result message associated with it. One way to represent this in Wing is to use a struct like this: struct NetworkState {
state: str;
code: num?;
result: str?;
}
let handleState = (state: NetworkState): Response => {
if state.state == "loading" {
log("loading...");
} else if state.state == "success" {
log("success: ${state.result ?? "<error>"}");
} else if state.state == "failure" {
log("failure code: ${state.code ?? 0}");
}
}; There's a couple of glaring issues with this code. The first issue is that no matter of what the network state is, it's still possible for me to access to the "code" and "result" fields, even though they shouldn't be accessed. If this is a struct I'm exposing publicly in a library, maybe I'd document a field like Another issue is that no matter what the state is, I still have to unwrap the let handleState = (state: NetworkState): Response => {
if state.state == "loading" {
log("loading...");
} else if state.state == "success" {
if let res = state.result {
log("success: ${res}");
} else {
throw("invalid network state");
}
} else if state.state == "failure" {
if let code = state.code {
log("failure code: ${code}");
} else {
throw("invalid network state");
}
}
}; But the largest issue is that, as given, the struct lets you represent invalid states. let s1 = NetworkState {
state: "success",
code: 404 // the success state cannot have an error code
}; One way to address this is to model class NetworkState {
static loading(): NetworkState {
new NetworkState("loading", nil, nil);
}
static success(message: str): NetworkState {
new NetworkState("success", message, nil);
}
static failure(code: num): NetworkState {
new NetworkState("failure", nil, code);
}
_state: str;
_message: str?;
_code: str?;
init(state: str, message: str?, code: str?) {
this._state = state;
this._message = message;
this._code = code;
}
state(): str {
return this._state;
}
message(): str {
if let message = this._message {
return message;
} else {
throw("cannot access message in state " + this._state);
}
}
code(): num {
if let code = this._code {
return code;
} else {
throw("cannot access code in state " + this._state);
}
}
}
// example usage
let s1 = NetworkState.failure(404);
assert(s1.state() == "failure");
assert(s1.code() == 404); This does adequately address the main problem, as it's no longer possible to represent invalid states inside instances of This is sort of like how you can work around not having generics in a language by creating a separate classes for ADTs make it straightforward to model information where fields are mutually exclusive, avoiding the mentioned problems: enum NetworkState {
Loading,
Success(str),
Failure(num),
}
let handleState = (state: NetworkState): Response => {
switch x {
Loading -> { log("loading..."); },
Success(msg) -> { log("success: ${msg}"); },
Failure(code) -> { log("failure code: ${code}"); },
};
}; |
Summary
No response
Feature Spec
As a Wing user, I would like to be able to express enums where each choice may have one or more associated fields.
Examples:
Such an enum could be used like so:
Since a switch statement is the only control flow that lets you safely unwrap an enum, it is the only way to extract the values of the associated fields.
The compiler would translate such an enum into an enum-like class in JavaScript:
And the enum usage would be translated like so:
FAQ
Q: What about ordinary enums without any fields?
A: Enums with no associated fields could continue to be compiled in a way so that they produce regular integer/string values. Alternatively, we could compile all enums into JavaScript classes, and give them a toString / valueOf so they play well with the rest of the JS ecosystem.
Q: How would this work with JSII?
A: When compiling a Wing library into a JSII module, complex enums would be turned into an enum-like class in the JSII type system. This way, it can be used safely in other languages. To use the exported library in Wing code, the Wing compiler's jsii importer would need to recognize enum-like classes produced by Wing (specifically, these must be enum-classes where there is a "discriminant" field/property -- we can't use
switch
on ordinary enum-like classes from AWS CDK or CDKTF libraries). If it is such a enum-like class, then it will be imported as an enum type in Wing's type system, otherwise it would be imported as an ordinary JSII class with static methods etc.Use Cases
See code examples above
Implementation Notes
References:
Component
Language Design
The text was updated successfully, but these errors were encountered: