A Case for Properties

November 15, 2020
~12m read time
November 15, 2020
~12m read time

Properties are a really nice feature of modern languages, and I've recently gotten attached to them in Kotlin. For a while I wasn't comfortable with the idea of 'fields' performing arbitrary computation, but having used them for a few months now I'm really enjoying the simplicity - especially when moving back to Java!

Having given it some thought, I wanted to lay out a case in support of properties which highlights their benefits over directly accessing fields (which is mostly about getters/setters, but nonetheless), and how we can establish some guidelines on when properties should not be used to avoid the issue of arbitrary computation causing side effects or other unexpected behavior.

What is a property?

In short, a property is syntax sugar for getter/setter methods that look like fields. Instead of having to use the methods item.getQuantity() and item.setQuantity(), we can access the property item.quantity and set it using item.quantity = 1, exactly like a field. The compiler transforms property access and assignment to use the appropriate getters/setters (hence, syntax sugar).

The most noteworthy consequence of this is, in my opinion, allowing validation on assigment. For example, item.quantity may be defined as follows (in Kotlin):

class Item {
    var quantity: Int = 0
        set(value) {
            require(value >= 0) //throws if negative
            field = value
        }
}

  
item.quantity = -10;

What's the benefit?

The largest benefit of properties is replacing field use, which has a massive amount of drawbacks, with methods instead. In languages like C++ and Java that don't have properties, the best practice is to keep all fields private and manually create getters and setters. More modern languages, like C# and Kotlin, use properties for this instead which can do that automatically.

Most of the following points are also benefits of using getters/setters over fields, and then properties step in to provide the extra bit of syntax sugar icing on the cake.

API Safety and Compatibility

  1. Property getters/setters can have different access modifiers, allowing for fields which are read-only for external use but still mutable within the class.
    • Example: In Kotlin, val counter = 0 private set defines a counter field that can be publicly accessed but not modified (unless within the class).
  2. Getters/setters can be used to maintain invariants and perform other validation work, such as quantity >= 0 in the opening example.
  3. Since getters/setters are methods, the implementation can be changed without breaking either API or ABI compatibility.
    • Example: A Vector class could have x/y/z properties, but change the implementation to use a component array [x, y, z] instead without issue.
    • Additionally, getters/setters can be inlined during optimization to speed up performance at the cost of ABI compatibility.

Polymorphic Behavior

  1. Properties can be used in interfaces since they're backed by methods, while fields require getters/setters to be defined for them. Notably, these getters/setters would still need to have fields defined in the implementing class to handle their values, while a property can handle that automatically.
  2. Properties can be used for methods which often behave like fields, but cannot be implemented with one due to subtypes requiring computation or other restrictions.
    • Example: List#size() generally doesn't need computation and returns the value of a field, but some implementations (such as a cons list) might.
    • Whether List#size() should be a property is certainly debatable, but with how frequently collections are used I think it's almost a necessity.

Ease of Use

  1. Properties are a single unit. Getters/setters are located in the same location in the source code as the property declaration, making it easier to fully understand a field as opposed to having them defined in separate locations.
  2. Properties almost always have syntax sugar for providing default implementations of get/set (if it's not already done automatically). In Kotlin, val property provides a default getter while var property provides both a getter and setter.
  3. Debugging properties is possible without a dedicated debugger by using getters/setters. This may be minor for large languages, but if your language doesn't have an IDE or still in development this is extremely useful.
  4. Readability is often increased, albeit debatable. I personally find item.quantity = 1 to be more readable than item.setQuantity(1), especially in large chains.
  5. Generally, properties imply 'simple' behavior, like fields, as opposed to complex computation that some methods may perform. We'll talk about this in detail soon.

So what are the downsides?

I think there are two notable downsides to having properties. First, the performance difference between field access and method calls can be significant, especially in low level languages. Second, field access has clear, well-defined behavior while a method call could effectively do anything, thus making it harder to reason about code. Let's dig in to both of these.

Problem 1: Performance

Field access is almost always language primitive, and is therefore faster than doing the work of a method call (which then has to access something anyways).

That said, compiler optimizations like inlining can easily resolve this for simple properties. In more complex cases, this is actually where properties are most useful - a field itself wasn't sufficient, so the alternative would have been a method call or sacrificing validation anyways.

In low level languages where memory layout is often important, knowing a field access corresponds to a memory lookup (and not arbitrary computation) can be important. In higher level languages, particularly Object Oriented ones that often encapsulate fields behind methods anyways, I don't believe this distinction is as necessary.

Problem 2: Arbitrary Computation

Finally, we've made our way to what I believe is the core issue: since properties can perform arbitrary computation, it is unclear what potential behavior getting/setting a property may have. The extreme example here, of course, is deleting the entire file system. Fields on the other hand have clearly defined behavior - get or set the value, nothing more.

However, these type of assumptions are not unique to properties. We apply the same assumptions to methods (and honestly everything) as well. I often presume that getX() and setX(x) have 'simple' behavior, but they could use lazy initialization, have unexpected validation checks, or cause side-effects and other unexpected behavior like a database getConnection(). Naming is one of the two hard problems in computer science, likely because we carry assumptions with us that may not always fit.

That said, these assumptions still have value and help us reason about code without having to memorize the API of the entire system. So, I thought about the assumptions I make about properties and what guidelines could be put in place to restrict what they should do. I'm not confident enough to claim these as rules (yet!), and I welcome any counterexample that these guidelines do not address.

With the main issue being the potential for arbitrary computation, the first guideline is...

  1. Properties should be pure, excluding state managed by the property itself. This is intended to allow lazy initialization and setting backing fields or delegated properties as needed, but restricts modifying unrelated state or performing other side effects.

Next up, let's revisit getConnection - this method not only causes side effects but can also fail if the database is unavailable (among others). This is something I believe to be beyond a property, as it's doing computational work the caller may have to recover from (emphasis here). As such, the next guideline is...

  1. Properties should not cause errors, excluding non-recoverable errors (failure). This includes state/argument validation and potential internal errors.

Lastly, the most controversial guideline which I think is best explained afterwards:

  1. Properties should be used for amortized constant-time operations, excluding inherited properties where the implementation can't support it. This is intended to avoid complex calculations while still allowing lazy initialization and list.size for something like a cons list. (though could be made amortized if cached).

This is certainly contrived to fit these cases, but I think this is the best justification I can give right now for why list.size is acceptable as a property while something like list.sorted() probably isn't. There's likely some odd cases I haven't thought of yet, like file.lines being okay if it returns a sequence (lazy computation) but a list result is probably not a good choice.

Example Use Cases

Keeping these guidelines in mind, I think it'd be useful to examine some example use cases of properties which I believe work really well. Both of these showcase properties being used to provide different views on data which can be used more effectively than through methods.

Example: RgbColor

This example uses properties to work with individual red, blue, and green components in a RGB hex color. These are virtual properties, and thus the only data being stored in memory is the hex value itself. A similar approach can be used for flags.

data class RgbColor(var hex: Int) {
    var red: Int
        get() = hex.and(0xFF0000).shr(16)
        set(value) {
            require(value in 0..255)
            hex = hex.and(0x00FFFF).or(value.shl(16))
        }
    var blue: Int = ...
    var green: Int = ...
}

  
val color = RgbColor(0xFF0000)
color.blue = 0xA5
println(color.hex.toString(16))

Example: Map Values

This example takes things a step further and uses properties to return a new object. The example below uses properties twice: once to provide a view of the map's values as a Collection<Int>, and a second time to provide another view which can be used to mutate the original map.


  
val map = mapOf("x" to 1, "y" to 2, "z" to 3);
map.values.mutable.map { it * it };
println(map);

For some reason, Kotlin's standard library doesn't do this so the code above naturally won't compile if you try it yourself. Maybe someday in Rhovas?

To me, the use of properties to do this with map.values.mutable.map { ... } is clearer than the alternative with methods, map.values().mutable().map { ... }, as the connection between the original map and the mutable view at the end is lost by the method calls. While Kotlin does support map.mapValues and related, these need to be duplicated for keys and entries and may be missing other helpful functions as well. Maybe this isn't something you should do anyways, but I think it's an interesting idea and works well enough in the above example.

Closing Thoughts...

I really enjoy working with properties in modern languages, and I hope this provides a fair case for why they can be a good language feature when used properly (no pun intended).

In quick summary, properties offer a large number of benefits over fields with respect to correctness, semantic capability, and usability with relatively few downsides in turn. The guidelines we established to avoid unexpected behavior with properties are:

  1. Properties should be pure, allowing for changes to backing state.
  2. Properties should not throw errors, allowing for unrecoverable failures.
  3. Properties should have ~linear time complexity, allowing for rare cases.

Feel free to reach out with questions or comments, and I'd love to hear feedback on the above guidelines and whether this post has impacted your opinion on properties.

  • Email: WillBAnders@gmail.com
  • Discord: Tag me in the #blog channel!

Thanks for reading!
~Blake Anderson