Skip to content

Latest commit

 

History

History
187 lines (135 loc) · 9.27 KB

enum-entries.md

File metadata and controls

187 lines (135 loc) · 9.27 KB

Decommission Enum.values() and replace it with Enum.entries

  • Type: Design proposal
  • Author: Vsevolod Tolstopytov
  • Status: Accepted
  • Prototype: Implemented
  • Discussion: KEEP-283
  • Related issues: KT-48872

This proposal describes a rationale and a path to migrate from function values() provided by each enum type to a collection-based and more predictable abstraction.

Motivation

Every Java and Kotlin enum type provides a values method that returns an array of all enum entries.

Arrays are mutable by default, meaning that each call to values() always has to allocate a new instance of the array. The API shape does not indicate that fact, often leading to hidden performance bugs in both Kotlin and Java. It is hard for library authors to determine whether an arbitrary call to values() may lead to a performance bottleneck as the profile depends on the "hotness" of the method, not on the enum's characteristics, effectively forcing authors to apply a workaround or leave it as a potential performance issue.

Apart from that, most of the API being written leverages Collections API, not arrays, forcing users to manually convert arrays to lists.

Acknowledgements of the problem

Examples of performance issues

Proposal

The proposal that addresses all of the above is as follows:

  • Decommission of Enum.values() with the IDE assistance without deprecation.
  • Introduce property entries: EnumEntries<E> that returns an unmodifiable list of all enum entries.
  • For already compiled Kotlin enums and Java enums, a special mapping class that contains a pre-allocated list of entries is generated.

Decommission of Enum.values()

Enums have been existing since the very beginning of Kotlin, and since Java 1.5, released in 2004, meaning that the deprecation of values using Kotlin's standard deprecation cycle will create unnecessary disturbance in the ecosystem and will render a lot of already existing educational materials outdated.

To avoid that, values() will be softly decommissioned via IDE assistance:

  • values will be de-prioritized and eventually removed from IDE auto-completion.
  • Soft warning with an intention to replace call to values() with call to entries will be introduced.
  • All the corresponding materials, such as Kotlin guides, J2K and tutorials will be adjusted to use entries API.
  • Eventually, we are going to re-visit this decision and decide on the further deprecation of values API.

EnumEntries<E> type

Effectively, entries represents an immutable list of enum entries and can be represented as type List<E>. To have an ability to further extend Enum's API in a non-intrusive manner that does not involve code-generation for each enum, we expose a direct subtype of List<E> named EnumEntries:

sealed interface EnumEntries<E : Enum<E>> : List<E>

We deliberately limit any extensions of this type to the standard library to have a future-proof way to extend it in the future in a backwards-compatible manner. All future potential extensions, such as valueOfOrNull(String), hasValue(String) can be implemented on the standard library level as members or extensions of EnumEntries.

Naming alternatives

Various languages use different notations to refer to enumeration elements, making the naming choice especially hard.

We have considered the following alternatives:

  • values property. While being the most familiar one, it carries the burdens of the existing values() API:
    • It is easy to misspel it with values() and fall into the same trap.
    • Introduction of values property adds an unfixable method reference ambiguity for all the callers of E::values.
  • Java language refers to enum elements as constants.
    • The risks of introducing widely-used API named constants with the potential clashes with Kotlin constant evaluation outweighs the benefits of the straightforward name.
  • valuesList and entryList suffer the same disease -- excessive verbosity of the name.
  • components is already reserved in Kotlin for positional deconstructors.
  • Kotlin specification and documentation already refer to enum elements as entries, making it our final choice.

Translation strategy for Kotlin enums

For source-compiled enums, a new static property with a JVM signature public static EnumEntries<E> getEntries() is added, returning a pre-allocated immutable list of enum entries.

For the sake of compatibility with bytecode pre-processors and reflection usages that modify a backing array $VALUES field, the generated bytecode for values() is left intact, and the resulting list of entries reads the underlying $VALUES lazily, leaving the chance for existing bytecode preprocessors to change the array without dealing with list allocation.

Implementation note

A special type, EnumEntriesList is introduced to the standard library. The type has the only constructor accepting the functional type with a signature () -> Array<E>. The corresponding lambda is generated using the standard invokedynamic with LambdaMetafactory.

The final decompiled class for the enum

enum class MyEnum {
    A;
}

has the following form (all irrelevant parts omitted):

enum MyEnum extends Enum<MyEnum> {
    private static final synthetic MyEnum[] $VALUES
    private static final synthetic EnumEntries<MyEnum> $ENTRIES;
   
    <clinit> {
        A = new MyEnum("A", 0);
        $VALUES = $values();
        Supplier<MyEnum[]> supplier = #invokedynamic ..args.. $entries;
        $ENTRIES = EnumEntries.Kt.enumEntries(supplier); // internal factory from standard library
    }

    public static MyEnum[] values() {
        return $VALUES.clone();
    }
  
    public static EnumEntries<MyEnum> getEntries() {
        return $ENTRIES;
    }

    private synthetic static MyEnum[] $entries() {
        return $VALUES;
    }

    private synthetic static MyEnum[] $values() {
        return new MyEnum[] { A };
    }
}

Translation strategy for compiled Kotlin enums and Java enums

For already compiled Kotlin enums and Java enums, a separate synthetic mapping classfile will be introduced for each callsite of Enum.entries. The mapping class acts as a storage for a lazily-initialized and pre-allocated list of enum entries.

Risks and assumptions

The proposal has two main risks:

The first is the potential source-compatibility issue for entries property in the companion object within an Enum. After the implementation of the proposed change as is, the following existing code will change its behaviour:

enum class MyEnum {
    A;
    
    companion object {
        val entries: Any? = ...
    }
}

MyEnum.entries // <- member has a higher priority than a companion member

To mitigate that, a separate compiler-assistant deprecation cycle is introduced, that will keep companion's entries priority higher than auto-generated memeber for the duration of the deprecation cycle. Taking into account the additional deprecation cycle and non-trivial setup for the problem to reproduce, we do not consider it a serious threat to compatibility.

The second risk is the education disturbance and a new name for developers to get familiar with — entries, opposed to already well-known values. It is mitigated by indefinitely long soft decommision instead of a regular deprecation cycle.

Collateral changes

In addition to an existing enumValues function in the standard library, enumEntries function that returns entries list is added. enumValues is deprecated for the removal.

Timeline

The feature is going to be available as experimental starting from Kotlin 1.8.0 and as stable starting from Kotlin 1.9.0. The corresponding language feature can be enabled with the -XXLanguage:+EnumEntries compiler argument in Kotlin 1.8.0.