I think we can go even bigger here. Instead of making the user realize that there is some special client required to interact with a construct at "runtime", why don't we use our compiler powers to determine what is required to capture. Each object should provide a full abstraction over the underlying resource, whether or not that needs to be done at "construction time" via, eg., CFN, or at "runtime" via, eg., the AWS SDK.
The model I'm considering here is more along the lines of mutability: objects have certain methods that will mutate itself and some that do not (post fact: I think inspired by Rust). Since wing is targeting multiple computation environments, the "initial" one is the only environment that can access the mutable methods. Other environments are only provided the immutable methods. We can do some fancy dependency analysis to determine what code needs to be captured in order to make these methods still function.
As an example, here is what a DynamoDB table would look like (the keyword to notice here is mut):
table.w
construct dynamodb.Client {
private sdk_client: ...
private table_name: token
init(mut self, table_name: token) {
self.sdk_client = ...
}
put_item(self, item) {
self.sdk_client.put_item({
TableName: self.table_name,
...item
})
}
}
construct dynamodb.CfnTable {
table_name: token
global_secondary_indices: list<...>
init(mut self) {
self.table_name = token(Arn.of(...))
}
synth(self): object {
return {
TableName: self.table_name,
...
}
}
}
construct dynamodb.Table {
private cfn_table: dynamodb.CfnTable
private client: dynamodb.Client
init(mut self, partition_key: string) {
self.cfn_table = dynamodb.CfnTable(partition_key)
self.client = dynamodb.Client(self.cfn_table.table_name)
}
add_global_secondary_index(mut self, ...) {
self.cfn_table.global_secondary_indices.push(...)
}
put_item(self, ...) {
self.client.put_item(...)
}
}
Then, a library consumer can create a construct that listens to a queue and adds new entries to the table using an immutable method.
register_user.w
construct RegisterUser {
init(mut self, users: dynamodb.Table) {
queue = sqs.Queue()
register_user = lambda.Function(process (user: object) -> {
users.put_item({
ID: { S: user.id },
UserName: { S: user.name }
})
})
queue.consume_messages(register_user)
}
}
This will get compiled to the following intermediate wing code using compiler analysis to figure out which objects need to be initialized and what dead code to be removed.
register_user.inter.w
/* no change */
construct dynamodb.Client {
private sdk_client: ...
init(mut self) {
self.sdk_client = ...
}
put_item(self, item) {
self.sdk_client...
}
}
construct dynamodb.CfnTable implements aws.CfnResource {
public table_name: token
/* different than source */
init(mut self) {
self.table_name = <complied token>
}
}
/* no change, besides removing add_global_secondary_index */
class dynamodb.Table {
private cfn_table: dynamodb.CfnTable
private client: dynamodb.Client
init(mut self, partition_key: string) {
self.cfn_table = dynamodb.CfnTable(partition_key)
self.client = dynamodb.Client()
}
put_item(self, ...) {
self.client.put_item({
TableName: self.cfn_table.table_name
...
})
}
}
/* argument generated from user-supplied function */
fun main(user: object) {
/* compiled init */
users = Table()
/* end compiled init */
/* user-supplied function */
users.put_item({ ID: { S: user.id }, UserName: { S: user.name } })
/* end user-supplied function */
}
Notably, the following will fail to compile:
register_user_bad.w
construct AddIndex {
init(mut self, users: dynamodb.Table) {
queue = sqs.Queue()
add_index = lambda.Function(process (id: string) -> {
users.add_global_secondary_index(string)
})
queue.consume_messages(add_index)
}
}
compiler output
error: cannot compile closure that references a mutable object
| add_index = lambda.Function(process (id: string) -> {
| users.add_global_secondary_index(string)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ `add_global_secondary_index` requires `users` to be mutable but it is immutable in this closure
Furthermore, immutable methods can be invoked in the "initial" computation environment. This goes along with the idea that initialization of some construct includes two main components: resource allocation and data injection, regardless of whether allocation means asking the OS for some memory or telling AWS to create a new resource.
(This entire idea will require some work around how we reference computation environments and which properties to capture automatically but figure that can wait until we decide this is the right direction to go.)
Originally posted by @BenChaimberg in https://github.com/monadahq/rfcs/pull/4#discussion_r918012091
I think we can go even bigger here. Instead of making the user realize that there is some special client required to interact with a construct at "runtime", why don't we use our compiler powers to determine what is required to capture. Each object should provide a full abstraction over the underlying resource, whether or not that needs to be done at "construction time" via, eg., CFN, or at "runtime" via, eg., the AWS SDK.
The model I'm considering here is more along the lines of mutability: objects have certain methods that will mutate itself and some that do not (post fact: I think inspired by Rust). Since wing is targeting multiple computation environments, the "initial" one is the only environment that can access the mutable methods. Other environments are only provided the immutable methods. We can do some fancy dependency analysis to determine what code needs to be captured in order to make these methods still function.
As an example, here is what a DynamoDB table would look like (the keyword to notice here is
mut):table.w
Then, a library consumer can create a construct that listens to a queue and adds new entries to the table using an immutable method.
register_user.w
This will get compiled to the following intermediate wing code using compiler analysis to figure out which objects need to be initialized and what dead code to be removed.
register_user.inter.w
Notably, the following will fail to compile:
register_user_bad.w
compiler output
Furthermore, immutable methods can be invoked in the "initial" computation environment. This goes along with the idea that initialization of some construct includes two main components: resource allocation and data injection, regardless of whether allocation means asking the OS for some memory or telling AWS to create a new resource.
(This entire idea will require some work around how we reference computation environments and which properties to capture automatically but figure that can wait until we decide this is the right direction to go.)
Originally posted by @BenChaimberg in https://github.com/monadahq/rfcs/pull/4#discussion_r918012091