8
$\begingroup$

Some languages support binding a function call to an attribute.

Python, for example, supports this via property

class Foo:
    _values = [1, 2, 3]

    @property
    def last_value(self):
        return self._values[-1]

    @last_value.setter
    def last_value(self, new_value):
        self._values[-1] = new_value

JavaScript supports this via get and set

class Foo {
    #values = [1, 2, 3]

    get lastValue() {
        return this.#values[this.#values.length - 1];
    }

    set lastValue(newValue) {
        this.#values[this.#values.length - 1] = newValue;
    }
}

When designing a language, why might I choose to provide support for this over (or in addition to) direct access and class functions?

$\endgroup$

3 Answers 3

8
$\begingroup$

Getters and setters are an almost indelible part of object-oriented design. Java can serve as a prime example of how this looks in a pure form:

class Foo {
    private int bar = 0;
    
    public int getBar() {
        return this.bar;
    }

    public void setBar(int newValue) {
        this.bar = newValue;
    }
}

What we do is simple - we have the private field value and then two methods to retrieve its value and give it new value.

This is boilerplate. And this boilerplate exists in a lot of Java classes. It is so ubiquitous because there are two things clashing:

  • Information should be isolated from the outside. Encapsulation is one of the core principles of object-oriented programming. But it is not even unique to OOP, making sure the internal state of a program is predictable and not open to be tinkered with from the outside has been recognised as important before OOP existed.
  • Controlling the access to a piece of information is also a concern. Thus the usage of getters and setters to allow intercepting these operations and potentially respond to them.

Java has no syntax or language support around fields. It really only has fields then methods that allow access to it. By convention the methods are called get<field name> and set<field name> but they are not really a "getter" and "setter" as they are no different from any other method that exists. You could, for example, make a property "immutable" publicly by not exposing a set<field name> method but at the end of the day, it is a method like any other.

With time, language designers realised this concept is important for object-oriented programming. For example, C# offers this more concise syntax for defining getters and setters:

class Foo {
    public int bar { get; set; }
}

It cuts down a lot of the required boilerplate code to define an otherwise very simple class. This is essentially the same as the following more expanded C# code:

class Foo {
    private int _bar;
    public int bar
    { 
        get
        {
            return this._bar;
        } 
        set
        {
           this._bar = value; 
        }
    }
}

This is much closer to the Java example. And much closer to the Python and JavaScript examples. It allows direct control over what the getter and setter do. Thus, for example, if the value needs to be manipulated before writing, or after retrieval, that can still be done.

What is different from Java is that it is directly baked into the language. Because the concept of getters and setters is so important to OOP, it would make sense to have direct support for it.

This allows more flexibility in terms of how instances are used. The API that the object provides is that there are some fields on the object and some methods you can call. There is no longer a mix of methods-that-only-return-a-field there.

In Java, or any language that does not have direct support for getters and setters, if you want to switch some value from being a simple exposed property to a computed thing, you need to change it from a field to a method and then also update all sites that use it from obj.field to obj.getField(). This is highly inconvenient.

However, if the language already has direct getter and setter support, the change can be made transparently without affecting any call site. In JavaScript

class CartItem {
    price = 5;
}

can be updated to

class CartItem {
    #price = 5;
    #quantity = 1;
    set quantity(number) {
        this.#quantity = number;
    }
    get price() {
        return this.#price * this.#quantity;
    }
}

without affecting any call sites that already use cartItem.price. However, now there is an option to update the quantity which will also reflect the price. Just another example how price can be entirely changed:

class CartItem {
    get price() {
        return lookupPrice(this);
    }
}

which turns the price field into a dynamic value. Thus prices can be maintained separately from the objects.

These are all available through direct getter and setter support. The important thing is that it better represents object-oriented encapsulation without having to adapt other features to emulate them (methods).


The consideration here is more or less around control:

Having getters/setters

It is important to remember that at the very least, this allows for tighter control over information. Exposing only a getter for a field means that external code cannot modify it. It might even be a feature to receive an error in this case.

Object oriented programming does make a big use of encapsulation, so this is probably a good fit for a more OO-leaning language.

Not having getters/setters

On the other hand, allowing access and modifications to fields can also be considered as a benefit by allowing more freedom. It might be especially useful in order to monkey-patch some things in the future by changing fields directly.

Early JavaScript did not have anything remotely like getters and setters. It was initially much more limited language with simple applications in mind. Hence why having all fields essentially publicly visible was in some ways a boon as simple applications could be changed also without having to do complex changes to the source code. Loading a script with an application, then loading a second script that modifies the first application was a somewhat common practice. So, a language that expects such changes might elect to not provide getters and setters that might be used to prevent them. It might also serve to intentionally limit the scope of the language to simpler constructs. This might be desirable for some audiences. Or to reduce the complexity of what would be built with it.

$\endgroup$
6
$\begingroup$

One-step modification

Swift actually supports three such keywords: get, set, and _modify.

struct Example {
  private var _x = 1

  var x: Int {
    get {
      print("getter called")
      return _x
    }
    set {
      print("setter called")
      _x = newValue
    }
    _modify {
      print("_modify called")
      yield &_x
    }
  }
}

var ex = Example()
_ = ex.x // getter called
ex.x = 2 // setter called
ex.x += 3 // _modify called

This allows more efficient access to some data structures. If the computed property is an array, you can yield a pointer to that array and append to it via the pointer. Without _modify, you would have to copy the array, append to the copy, and then overwrite the original. This optimization is clunky at best to support via functions rather than accessors.

$\endgroup$
1
  • $\begingroup$ Technically there's a fourth, _read, but the reasons for its existence are far beyond the scope of this answer. $\endgroup$
    – Bbrk24
    Commented May 19, 2023 at 22:01
3
$\begingroup$

When designing a language, why might I choose to provide support for this over (or in addition to) direct access and class functions?

Properties let you have the nice look/feel of a direct ivar access, without the downside. At any point you can replace a call to a stored property with a call to a computed property with the same name. They expose an interface, and hide the implementation.

Contrast this with direct ivar access: once you allow it, the cat's out of the bag. You have to keep that stored ivar in your object, whether it fits your new design goals or not, because callers are depending on it.

I would point to Swift's take on this, which I think solves this really well: just don't allow ivar access in the first place (apart from some opt-in cases for performance like with @frozen). It's all properties, allowing the changing between stored and computed without API-breaking changes.

In the general case, there's a small performance downside to this: each property access is more like a function call than it is a memory read. On the bright side, within a module, the optimizer can trivially optimize every stored property call to a direct ivar access. Between modules you pay some cost, but you get good value for it: the other module can change its properties' implementation without you being affected.

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .