Accessing type internal fields in package interfaces

Cost of public properties

  1. In my view, the main advantage of Julia’s multiple dispatch is that it allows everybody equal ability to define public functions of x::X, rather than privileging the owner of X. As a function author, I can write new functions that look and feel like they belong on x just as much as X’s author can.

    Properties are similar to OOP’s X().f() methods. They are in a namespace controlled by the author of X. The symmetric extensibility permitted by multiple dispatch doesn’t work nearly as well if properties are part of a public interface because they privilege a single argument and its getproperty method.

    A function author cannot define properties that look and feel native to X without piracy.

  2. It is useful when writing a package to think about the public interface separately from the implementation. Public properties reduce the delineation between interface and implementation, and may cause implementation details to leak into interfaces unnecessarily.

So public properties come at a significant cost.

Benefits of public properties

  1. At the definition site, using properties means authors don’t have to write out the definition of the getters:
struct Person
name
age
end

is shorter than

struct Person
name
age
end
name(p::Person) = p.name
age(p::age) = p.age
  1. At the call site, person.name needs one fewer character than name(person).

  2. At the call site, person.name |> length puts the property on the right rather than the left of name(person) |> length, so chains are read linearly in left-to-right order.

  3. At the call site, if multiple packages in scope provide functions name for their objects, they need to be used in package-namespaced form Persons.name(person) or imported under a different name like using Persons: name as pname, while person.name does not.

  4. At the call site, dot syntax can hint at certain information about a property access. Namely, person.name indicates it does not require expensive computation, does not raise an error, and does not change its value unless otherwise mutated.

Analysis

Benefits 1,2,3 are issues of surface syntax that can be solved with a macro or operator, without paying Cost 1 of public properties.

For Benefit 1,

@getters struct Person
name
@nogetter age
end

could define name(p::Person) = p.name without the user having to write it out.

For Benefit 2 and Benefit 3, various packages such as Chain.jl have been exploring chaining interfaces.

For example,

using Chain

struct Person
name
age
end
name(p::Person) = p.name

> person = Person("Alice", 99);
> @chain person name length
# 5

This chain is read linearly in left-to-right order (Benefit 3).
The chain doesn’t require parentheses (Benefit 2) though it does require @chain. The function-on-the-right invocation can also be written person|>name in the same number of characters as name(person) and one more than person.name.

A bigger change would be making a property p only accessible via function call p(x), and x.p just a shortcut equivalent to @chain x p. If desired, x.p might automatically disambiguate to parentmodule(x).p(x) if there is another p in the calling scope. This would require experimentation to see where it causes breakage.

Benefit 4 (name collisions) remains but I believe it is small because the function import can be handled once at the top of the caller’s file.

Benefit 5 (property hinting) seems hardest to attain without dot-access properties, because hinting at those properties is essentially a form of documentation rather than a fact about the code. This could be done with prose documentation, or a form of code specification adjacent to each function definition.

Conclusions

Most of the advantages of public properties can be obtained in a functional API without the downsides. Some changes will make that easier:

  • Make it easier to define getters and setters. Promote the use of @getters where appropriate and consider including it in Base.

  • Make it easier to chain calls. Promote |> and Chain.jl.

  • For radicals duly cautioned, consider making a property p only accessible via function call, and x.p equivalent to @chain x p.

  • Examine structured ways of specifying function properties like “constant value”, “low-cost access”, and “non-erroring”, and more.

4 Likes