Properties

A property is a special syntax. It does not store values like a field. Instead, it provides a getter and an optional setter to indirectly retrieve and set values.

By using properties, data operations can be encapsulated into access functions. The use of properties is the same as that of common fields, which means that you can perform data operations without being aware of the underlying implementation. In this way, mechanisms such as access control, data monitoring, tracing and debugging, and data binding can be implemented easily.

Properties have the same syntax as fields and can be used as expressions or assigned values.

The following is a simple example where b is a typical property that encapsulates external access to a.

class Foo {
    private var a = 0
    mut prop b: Int64 {
        get() {
            print("get")
            a
        }
        set(value) {
            print("set")
            a = value
        }
    }
}

main() {
    var x = Foo()
    let y = x.b + 1 // get
    x.b = y // set
}

Property Syntax

The syntax rules of properties are as follows:

propertyDefinition
    : propertyModifier* 'prop' identifier ':' type propertyBody?
    ;

propertyBody
    : '{' propertyMemberDeclaration+ '}'
    ;

propertyMemberDeclaration
    : 'get' '(' ')' block end*
    | 'set' '(' identifier ')' block end*
    ;

propertyModifier
    : 'public'
    | 'private'
    | 'protected'
    | 'internal'
    | 'static'
    | 'open'
    | 'override'
    | 'redef'
    | 'mut'
    ;

Properties without the mut modifier require a getter implementation. Properties declared by the mut modifier require separate getter and setter implementations.

In particular, you cannot define properties modified by mut or implement interfaces that have mut properties within extensions or definition bodies of the numeric, Bool, Unit, Nothing, Rune, String, Range, Function, Enum, and Tuple types.

In the following example, a is a property not declared by mut, and b is a property declared by mut.

class Foo {
    prop a: Int64 {
        get() {
            0
        }
    }
    mut prop b: Int64 {
        get() {
            0
        }
        set(v) {}
    }
}

Properties not declared by mut do not have setters. Like fields declared by let, they cannot be assigned values.

class A {
	prop i: Int64 {
		get() {
			0
		}
	}
}
main() {
    var x = A()
    x.i = 1 // error
}

Specifically, when a struct instance is declared by let, you cannot assign values to properties in the struct, just like fields declared by let.

struct A {
	var i1 = 0
    mut prop i2: Int64 {
        get() {
            i1
        }
        set(value) {
            i1 = value
        }
    }
}
main() {
    let x = A()
    x.i1 = 2 // error
    x.i2 = 2 // error
}

Properties differ from fields in that they cannot have initial values assigned and must have their types declared.

Property Definition

Properties can be defined in interfaces, classes, structs, enums, and extends.

class A {
    prop i: Int64 {
        get() {
            0
        }
    }
}

struct B {
    prop i: Int64 {
        get() {
            0
        }
    }
}

enum C {
    prop i: Int64 {
        get() {
            0
        }
    }
}

extend A {
    prop s: String {
        get() {
            ""
        }
    }
}

You can declare an abstract property in an interface and abstract class, and its definition body can be omitted. When an abstract property is implemented in an implementation type, its name, type, and mut modifier must remain unchanged.

interface I {
    prop a: Int64
}

class A <: I {
    public prop a: Int64 {
        get() {
            0
        }
    }
}

Just as abstract functions in an interface can have default implementations, abstract properties in an interface can also have default implementations.

When an abstract property has a default implementation, the implementation type is not required to provide its own implementation (as long as it complies with the usage rules of the default implementation).

interface I {
    prop a: Int64 { // ok
        get() {
            0
        }
    }
}
class A <: I {} // ok

Properties are classified into instance member properties and static member properties. Instance member properties can be accessed only by instances. You can use this expression in the getter or setter implementation of the property to access other instance members and static members. You cannot access instance members in the implementation of static member properties.

class A {
    var x = 0
    mut prop X: Int64 {
        get() {
            x + y
        }
        set(v) {
            x = v + y
        }
    }
    static var y = 0
    static mut prop Y: Int64 {
        get() {
            y
        }
        set(v) {
            y = v
        }
    }
}

Properties do not support overloading or overriding, and they cannot have the same name as other members at the same level.

open class A {
	var i = 0
	prop i: Int64 { // error
		get() {
			0
		}
	}
}
class B <: A {
    prop i: Int64 { // error
		get() {
			0
		}
	}
}

Property Implementation

The getter and setter of a property correspond to two different functions.

  1. The getter function has the type ()->T, where T is the type of the property. When the property is used as an expression, the getter function is executed.
  2. The setter function has the type (T)->Unit, where T is the type of the property, and the parameter name must be explicitly specified. The setter function is executed when the property is assigned a value.

The implementation rules of properties are the same as those of functions. Properties can contain declarations and expressions, return can be omitted, and the return value must comply with the return type.

class Foo {
    mut prop a: Int64 {
        get() { // () -> Int64
            "123" // error
        }
        set(v) { // (Int64) -> Unit
            123
        }
    }
}

The behavior of accessing a property is consistent both inside and outside the property. Therefore, recursive access to a property may cause an infinite loop, which is the same as that of a function.

class Foo {
    prop i: Int64 {
        get() {
            i // dead loop
        }
    }
}

Note that the setter of struct is a mut function. Therefore, you can modify the values of other fields in the setter, and this is restricted by the mut function.

Modifiers for Properties

Similar to functions, properties can be modified using modifiers. However, only the entire property can be modified. The getter or setter cannot be modified independently.

class Foo {
	public mut prop a: Int64 { // ok
        get() {
            0
        }
        set(v) {}
    }
    mut prop b: Int64 {
        public get() { // error
            0
        }
        public set(v) {} // error
    }
}

Access control modifiers private, protected, and public can be used for properties.

class Foo {
    private prop a: Int64 { // ok
        get() { 0 }
    }
    protected prop b: Int64 { // ok
        get() { 0 }
    }
    public static prop c: Int64 { // ok
        get() { 0 }
    }
}

Like an instance function, an instance property can be modified using open and override.

For properties modified by open, a subtype can use override to override the implementation of the supertype (override is optional).

open class A {
    public open mut prop i: Int64 {
        get() { 0 }
        set(v) {}
    }
}
class B <: A {
    override mut prop i: Int64 {
        get() { 1 }
        set(v) {}
    }
}

Static properties, like static functions, can be modified using redef (redef is optional), allowing a subtype to re-implement the static properties from the supertype.

open class A {
    static mut prop i: Int64 {
        get() { 0 }
        set(v) {}
    }
}
class B <: A {
    redef static mut prop i: Int64 {
        get() { 1 }
        set(v) {}
    }
}

When a subtype uses override or redef on properties that are declared by let, it must re-implement the getter.

When a subtype uses override or redef on properties that are declared by the mut modifier in the supertype, it is allowed to re-implement only the getter or the setter, or both.

open class A {
    public open mut prop i1: Int64 {
        get() { 0 }
        set(v) {}
    }
    static mut prop i2: Int64 {
        get() { 0 }
        set(v) {}
    }
}

// case 1
class B <: A {
    public override mut prop i1: Int64 {
        get() { 1 } // ok
    }
    redef static mut prop i2: Int64 {
        get() { 1 } // ok
    }
}

// case 2
class B <: A {
    public override mut prop i1: Int64 {
        set(v) {} // ok
    }
    redef static mut prop i2: Int64 {
        set(v) {} // ok
    }
}

// case 3
class B <: A {
    override mut prop i1: Int64 {} // error
    redef static mut prop i2: Int64 {} // error
}

When a subtype uses override or redef on properties, it must retain the same mut modifier and the same type as that in the supertype.

class P {}
class S {}

open class A {
    open prop i1: P {
        get() { P() }
    }
    static prop i2: P {
        get() { P() }
    }
}

// case 1
class B <: A {
    override mut prop i1: P { // error
        set(v) {}
    }
    redef static mut prop i2: P { // error
        set(v) {}
    }
}
// case 2
class B <: A {
    override prop i1: S { // error
        get() { S() }
    }
    redef static prop i2: S { // error
        get() { S() }
    }
}

When a subtype uses override on a supertype's property, it can use super to call the instance property of the supertype.

open class A {
    open prop v: Int64 {
        get() { 1 }
    }
}
class B <: A {
    override prop v: Int64 {
        get() { super.v + 1 }
    }
}