Skip to content

Latest commit

 

History

History
355 lines (262 loc) · 13.7 KB

explicit-backing-fields.md

File metadata and controls

355 lines (262 loc) · 13.7 KB

Explicit Backing Fields

  • Type: Design Proposal
  • Author: Nikolay Lunyak, Roman Elizarov
  • Contributors: Svetlana Isakova, Kirill Rakhman, Dmitry Petrov, Roman Elizarov, Ben Leggiero, Matej Drobnič, Mikhail Glukhikh, Nikolay Lunyak
  • Status: Prototype implemented in FIR
  • Initial YouTrack Issue: KT-14663
  • Initial Proposal: private_public_property_types#122

Summary

Sometimes, Kotlin programmers need to declare two properties which are conceptually the same, but one is part of a public API and another is an implementation detail. This pattern is known as backing properties:

class C {
    private val _elementList = mutableListOf<Element>()

    val elementList: List<Element>
        get() = _elementList
}

With the proposed syntax in mind, the above code snippet could be rewritten as follows:

class C {
    val elementList: List<Element>
        field = mutableListOf()
}

Table of contents

Use Cases

This proposal caters to a variety of use-cases that are currently met via a backing property pattern.

Expose read-only subtype

We often do not want our data structures to be modified from outside. It is customary in Kotlin to have a read-only (e.g. List) and a mutable (e.g. MutableList) interface to the same data structure.

internal val _items = mutableListOf<Item>()
val item : List<Item> by _items

And the new syntax allows us to write:

val items: List<Item>
    internal field = mutableListOf()

This use-case is also widely applicable to architecture of reactive applications:

  • Android LiveData has a MutableLiveData counterpart.
  • Rx Observable has a mutable Subject counterpart.
  • Kotlin coroutines SharedFlow has a MutableSharedFlow, etc.

For example, sample code from an Android app:

private val mutableCity = MutableLiveData<String>().apply { value = "" }
private val mutableCharts = MutableLiveData<List<Chart>>().apply { value = emptyList() }
private val mutableLoading = MutableLiveData<Boolean>().apply { value = false }
private val mutableMessage = MutableLiveData<String>()

val city: LiveData<String> = mutableCity
val charts: LiveData<List<Chart>> = mutableCharts
val loading: LiveData<Boolean> = mutableLoading
val message: LiveData<String> = mutableMessage

Becomes:

val city: LiveData<String>
    field = MutableLiveData().apply { value = "" }
val charts: LiveData<List<Chart>>
    field = MutableLiveData().apply { value = emptyList() }
val loading: LiveData<Boolean>
    field = MutableLiveData().apply { value = false }
val message: LiveData<String>
    field = MutableLiveData()

private is a default access for a field declaration.

In this use-case, an read access to the field from inside the corresponding classes/modules (where the field is visible) automatically gives access to mutable type, which we call Smart Type Narrowing, but is seen as a property with read-only type to the outside code.

Decouple storage type from external representation

Sometimes a property must be internally represented by a different type for storage-efficiency or architectural reason, while having a different outside type. For example, API requirements might dictate that the property type is String, but if we know that it always represents a decimal integer, then it can be efficiently stored as such with custom getter and custom setter.

val number: String
    field: Int = 0
    get() = field.toString()
    set(value) { field = value.toInt() }

Expose read-only view

In some application it is desirable to expose not just a read-only subtype, but a specially constructed read-only view that protects the data structure from casting into mutable type. For example a MutableStateFlow has asStateFlow extension for that purpose.

With the proposed syntax it is possible to declare a custom getter:

val state: StateFlow<State> 
    get() = field.asStateFlow()
    field = MutableStateFlow(State.INITIAL)

For this use-case, the TBD syntax of Direct Backing Field Access will need to be added.

Access field from outside of getter and setter

Kotlin allows property field to be accessed from the property's getter and setter using a field variable. This proposal is designed with an idea to provide an explicit syntax to access a property's field from anywhere inside the corresponding class when the field is declared explicitly:

class Component {
    var status: Status
        field // explicit field declaration
        set(value) {
            field = value
            notifyStatusChanged()
        }
}

This way, all the code inside the class can change the field of the property directly, without invoking the setter . However, the actual access syntax of such Direct Backing Field Access is TBD.

Design

The proposed design consists of two new ideas: explicit backing fields and smart type narrowing.

Explicit Backing Fields

The grammar for propertyDeclaration is extended to support an optional explicit backing field declaration in addition to the optional getter and setter (in any order between them).

propertyDeclaration ::=
    modifiers? ('val' | 'var') typeParameters?
    (receiverType '.')?
    (multiVariableDeclaration | variableDeclaration)
    typeConstraints? (('=' expression) | propertyDelegate)? ';'?
     ( (getter? (semi? setter)? (semi? field)?) 
     | (setter? (semi? getter)? (semi? field)?)
     | (getter? (semi? field)? (semi? setter)?) 
     | (setter? (semi? field)? (semi? getter)?)
     | (field? (semi? getter)? (semi? setter)?) 
     | (field? (semi? setter)? (semi? getter)?)

getter ::=
    modifiers? 'get' ('(' ')' (':' type)? functionBody)?
  
setter ::=
    modifiers? 'set' ('(' functionValueParameterWithOptionalType ','? ')' (':' type)? functionBody)?
    
field ::=
    modifiers? 'field` (':' type)? ('=' expression)?       

Explicit backing field declaration has an optional visibility, an optional type, and an optional initialization expression.

Restrictions

There are the following additional semantic restrictions on the property declaration grammar:

  • A property with an explicit field declaration cannot have its own initializer. A property with an explicit field must be initialized with the initialization expression for its field to clarify the fact, that property initialization goes directly to the field and does not call property's setter. The property without field initialization expression is considered uninitialized and is allowed only when it is lateinit (see Lateinit section for details).
  • A property with an explicit backing field must always explicitly specify the type of the property itself.
  • Explicit backing field declaration is not allowed for interface properties, for abstract properties, and for delegated properties.

A backing field type is not required to be explicitly specified:

  • If both backing field type and initialization expression are not specified, then the field type is the same as the property type.
  • If backing field type is not specified, but there is an initialization expression, then
    the field type is inferred from the type of its backing field initialization expression.
  • When both field type and initialization expression are specified, then the type of the former must be assignable to the latter.

Backing field assignability is the same as it is now for field references in getters and setters:

  • var properties have mutable backing fields.
  • val properties have read-only backing fields. That is, assignment to field inside a val property getter results in an error.

Accessors

When explicit backing field with type F for a property with type P is declared explicitly, then compiler can derive getter and, for var properties, setter implementation automatically:

  • If P :> F, the compiler can provide a default getter.
  • If P <: F, the compiler can provide a default setter.

If the compiler can not provide a getter, the user must declare it explicitly. The same applies to setters in case of var properties.

public val flow: SharedFlow<String>
    field: MutableSharedFlow? = null
    get() { // It is an error if getter is not explicitly specified here
        return field ?: run { ... }
    }

Visibility

Only the private and internal visibilities are allowed for explicit backing fields. The default field visibility is private.

val mutableWithinModule: List<Item>
    internal field = mutableListOf()

The special syntax to explicitly access the backing field from outside the code of property accessors is TBD, but the field can be implicitly access when it is visible via the Smart Type Narrowing.

Lateinit

If a property has an explicit backing field declaration, and it needs to be lateinit, the modifier must be placed at the field declaration.

var someStrangeExample: Int
    lateinit field: String
    get() = field.length
    set(value) {
        field = value.toString()
    }

Smart Type Narrowing

The idea behind smart type narrowing is to implicitly access the underlying field, as opposed to the property, when the field is in scope and when it is safe to do so. For example, expanding on Expose read-only subtype use-case, one can write:

class Component {
  val items: List<Item>
      field = mutableListOf()
  
  fun addItem(item: Item) {
      items += item // works
  }
}

// outside code
component.items += item // does not compile; cannot add to List<Item>

The code above works, because items there implicitly refers to the field with type MutableList<Item>.

Smart type narrowing works when trying to read the value of the property and all the following conditions are met:

  • The backing field is visible from the point of access.
  • The property is final (that is, it is not open open).
  • The property does not have an explicit getter.

The last rule automatically guarantees that the getter was automatically generated. Together with the requirement that the field is not open, it means that the compiler knows that the field stores the same instance as returned by the getter and that the type of the field is narrower than the type of the property (see Accessors section).

In this case, the type of property read expression is narrowed by the compiler from the type of the property to the type of its field.

Alternatives

Initial Proposal

Initially, the following syntax was suggested:

private val items = mutableListOf<Item>()
    public get(): List<Item>

In above example items is MutableList<Item> when accessed privately inside class and read-only List<String> when accessed from outside class.

In fact, the above syntax brings in an incorrect mental model: it says 'There is a private property items with some part that declares its public behavior'.

Attempt to add support for the above syntax led to multiple redundant complications (see the problems section and unclear override mechanics).

Future Enhancements

Direct Backing Field Access

We plan to add support for a syntax to explicitly access the property's backing field when the field was explicitly declared and is accessible via some TBD syntax.

Protected Fields

The set of visibilities for an explicitly declared field can be extended to include protected (in addition to private and internal). This way, subclasses can explicitly or implicitly (via the smart type narrowing) reference the field.

Mutable Fields for Read-only Properties

Letting a val property have a mutable backing field may be useful. Consider the following snippet from the implementation of lazy in the Kotlin standard library:

// backing field pattern
private var _value: Any? = UNINITIALIZED_VALUE

override val value: T
    get() {
       // initializes _value backing field on the first access
    }