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.
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;java.lang.IllegalArgumentException: Failed requirement. at Item.setQuantity(Item.kt:4) //require(value >= 0)
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.
- 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 acounter
field that can be publicly accessed but not modified (unless within the class).
- Example: In Kotlin,
- Getters/setters can be used to maintain invariants and perform other
validation work, such as
quantity >= 0
in the opening example. - Since getters/setters are methods, the implementation can be changed without
breaking either API or ABI compatibility.
- Example: A
Vector
class could havex/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.
- Example: A
- 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.
- 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.
- Example:
- 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.
- 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 whilevar property
provides both a getter and setter. - 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.
- Readability is often increased, albeit debatable. I personally find
item.quantity = 1
to be more readable thanitem.setQuantity(1)
, especially in large chains. - 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.
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.
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.
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...
- 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...
- 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:
- 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.
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.
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))0xFFA500 //blue is now 0xA5
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);{x = 1, y = 4, z = 9} //map is mutated
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.
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:
- Properties should be pure, allowing for changes to backing state.
- Properties should not throw errors, allowing for unrecoverable failures.
- 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