pkl icon indicating copy to clipboard operation
pkl copied to clipboard

`Reference<T>`: Type safe "deferred" references

Open HT154 opened this issue 11 months ago • 2 comments

One common pattern in infrastructure-as-code systems like Terraform and Pulumi is "references". A reference provides a way to refer to a resource (or a property thereof) that exists at a time after Pkl evaluation completes.

Example: The pulumi aws-yaml-static-website example creates a aws:s3:BucketV2 and creates a aws:s3:BucketPolicy that both directly references the bucket and a property of the bucket (its arn). In the case of the Pulumi YAML runtime, references are rendered as String sequences beginning with ${ and ending with }.

It would be useful for a Pkl module representing the aws:s3:BucketPolicy resource to define its bucket like so:

bucket: String | // accept a bucket name directly
  Reference<String> | // accept a reference to a string, possibly from another resource or function's output
  Reference<Resource> // accept a reference to a aws:s3:BucketV2 or another resource, using its ID (a String)

There should be an API within the standard library for representing and producing such references. How a reference is rendered or otherwise consumed would be module-dependent. At minimum, the Reference API should look something like this:

// members must be functions to avoid ambiguities between reading reference info and construction property "sub-references"
class Reference<T> {
  /// The Class that this underlying Reference points to.
  function getRootClass(): Class<T>

  /// The concrete value of the "root" of the reference.
  function getRootData(): Any

  /// The "path" of properties/keys referenced from the "root" value.
  function getPath(): List<PropertyReference|SubscriptReference>
}

class PropertyReference {
  property: String
}

class SubscriptReference {
  key: Any
}

function Reference<T>(root: T): Reference<T> = new {
  // clazz = root.getClass()
  // data = root
}

Reference should also exhibit covariance: given a class T and a subclass class U extends T, Reference<U> should be a subtype of Reference<T>.

It may also be beneficial to add syntax sugar for producing references, so these two expressions would be equivalent:

Reference(someValue).propA["key1"].propB[0]
&someValue.propA["key1"].propB[0]

This requires a SPICE to be written to iterate on the design.

HT154 avatar Jan 28 '25 04:01 HT154

Thanks for getting the ball rolling.

The basic idea here is that a Reference<T> would have the same property accessors as T, except all accessors continue to give you a Reference.

class Person { pet: Pet }

class Pet { name: String }

hidden bob: Reference<Person> = makeRef(Person, "bob")

petName = bob.pet.name // `petName is a Reference<String>`

Users would use output converters when rendering this. For example, maybe something like:

output {
  renderer = new YamlRenderer {
    converters {
      [Reference] = (it) -> "%{" + it.getPath().join(".") + "}"
    }
  }
}

Produces:

petName: %{bob.pet.name}

bioball avatar Jan 28 '25 14:01 bioball