Property capabilities#
Properties and indexers declared readonly or writeonly grant read and write access independently. A type then describes the authority an API needs instead of turning every member into a read-write slot.
local interface Snapshot
readonly value: string
end
local interface Output
writeonly value: string
endA Snapshot can read value but cannot assign it. An Output can assign the member but cannot observe its current value. See Interfaces for the rest of what an interface declares.
Declaring a capability#
The same syntax works in a record:
It works in a structural shape too, which is how a caller states the authority it needs without naming the declaration that supplies it:
local input: {
readonly value: string
} = Cell{value = "ready"}
local output: {
writeonly value: string | integer
} = Cell{value = "ready"}The two declarations name one runtime property. They may use different types: here a write accepts string | integer, while every read produces string. Construction may initialize a read-only record field, because the capability governs access through the constructed view rather than creation of the value.
An unmodified property is shorthand for matching read and write capabilities, so Ordinary and Expanded describe the same authority:
local type Ordinary = {
value: string
}
local type Expanded = {
readonly value: string,
writeonly value: string
}Dive deeper
Splitting the read type from the write type is what lets a normalizing setter be described rather than approximated. A property that accepts string |
integer and stores a string needs both types written down, and collapsing the pair to a single type has to pick which half to misstate: widening reads makes every consumer test a type the value never has, and narrowing writes rejects calls the implementation accepts. Declaration files for untyped Lua hit this on almost every setter, which is why the pair is part of the property rather than a separate declaration form.
Variance#
Readonly types are covariant. If Dog fits Animal, then {readonly value: Dog} fits {readonly value: Animal}, because every value read through it is still an animal.
Writeonly types are contravariant. A {writeonly value: Animal} fits {writeonly value: Dog}, because it accepts every dog the narrower view may write.
An ordinary property has both constraints, so it is invariant:
local interface Animal
name: string
end
local record Dog is Animal
name: string
end
local kennel: {value: Dog} = {value = new Dog(name = "rex")}
-- NUPP2001: {value: Dog} is not a {value: Animal}
local pen: {value: Animal} = kennelCode holding pen could write another kind of animal and break the type kennel has. Fresh table literals may initialize a contextual type, because no narrower stored view exists yet.
Indexers#
Indexers take the same capabilities, in shapes, interfaces, and records:
local interface ByteView
readonly [integer]: uint8
end
local interface ByteSink
writeonly [integer]: uint8
endA split pair works here too, so a normalizing map states what it accepts apart from what it returns:
local type Normalizing = {
readonly [string]: string,
writeonly [string]: string | integer
}Reading a map-like indexer stays optional because a key may be absent. The write type describes a present value accepted by assignment. See Primitive types for how map and array types are written.
Other qualifiers#
Property capabilities are member-level access views. The qualifiers written next to them answer different questions:
const Tmakes the whole value read-only rather than selecting members. See Primitive types for the view it produces.borrowsandexclusivegovern lifetime and aliasing, not whether a member may be read or written. See Ownership and affine types for the model.- A
constbinding prevents rebinding the local name. It does not by itself make the referenced table immutable.
Access diagnostics#
A read through a write-only view, and an assignment through a read-only view, are both reported. Compound assignment needs both capabilities, because it first reads the old value and then writes the result:
local record Counter
value: integer
end
local sink: {writeonly value: integer} = new Counter(value = 0)
sink.value = 1
sink.value += 1 -- NUPP2009: `+=` reads `value` through a write-only viewDuplicate capabilities are reported, as is an ordinary property combined with a separate capability of the same name, and so is a capability property on a struct. Struct fields are fixed C memory slots, so they remain ordinary invariant fields. See Records and structs for what a struct field may hold.