Skip to content

Converge inflight and mutability? #64

Description

@eladb

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status
    ✅ Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions