JuliaLang and Private Struct Fields#

TL;DR: There are none. But we can try anyways.

It’s a well-known fact that Julia does not support private fields in structs1. It’s somewhat similar to Python in this regard: Package maintainers can warn people not to touch certain parts of the code, but ultimately nothing’s stopping a curious user from poking around.

That answer may not be very satisfying to some people though. What’s the closest we can get to private fields? Can we have a few slightly stronger options than putting underscores in front of field names? In this post I’ll share two techniques I’m aware of that can discourage (but not stop) users from accessing certain struct fields:

  1. Create methods for setproperty! and getproperty

  2. Use @var_str

I make no claims about whether or not these methods are useful or idiomatic.

Note

In the code blocks that follow I assume that each line is entered into the Julia REPL, but I omit the julia> prompt text. Output is shown as commented text after the inputs.

Technique 1: Create new methods for setproperty! and getproperty#

First, the traditional method. Let’s create a struct with two fields:

struct mystruct
    field1
    field2
end

We can now create instances of this struct:

x = mystruct("value_1" , "value_2")

# mystruct("value_1", "value_2")

Suppose we really don’t want users accessing x.field2. Recall that Julia translates x.field2 into the function call Base.getproperty(x, :field2). Using the powers of multiple dispatch, we can specialize the Base.getproperty to behave differently when x is of type mystruct:

function Base.getproperty(x::mystruct, y::Symbol)
    if y === :field2
        error("This field is private.")
    else
        getfield(x, y)
    end
end

This code tests to see if the field being accessed is field2. If so, it errors. Otherwise it returns the field.

Does it work?

x.field2

# ERROR: This field is private.

Fantastic. How about accessing field1?

x.field1

# "value_1"

Success! Users can access x.field1, but they can no longer access x.field2 without getting yelled at.

Unfortunately the Achilles heel to this method is glaringly apparent in our method definition for Base.getproperty above. Remember that call getfield(x,y)? What happens if we use that function directly with x and field2?

getfield(x, :field2)

# "value_2"

Drat. Well, it’s not truly private, but this at least puts a little extra friction into accessing x.field2. Some users likely won’t even be aware that getfield exists.

Now if we were dealing with a mutable struct, there would be one more aspect to worry about: setting field values. The method above prevents users from accessing field values, but doesn’t prevent the user from changing them. As an example, let’s try creating a new mutable struct and using the getproperty trick above to discourage access to field2:

mutable struct mutablestruct
    field1
    field2
end

function Base.getproperty(x::mutablestruct, y::Symbol)
    if y === :field2
        error("This field is private.")
    else
        getfield(x, y)
    end
end

x = mutablestruct("value_1", "value_2")

# mutablestruct("value_1", "value_2")

Can we access x.field2?

x.field2

# ERROR: This field is private.

Awesome. Can we set x.field2 to a different value?

x.field2 = "not_value_2"

# "not_value_2"

Uh…

getfield(x, :field2)

# "not_value_2"

What?

We can’t access the field with . notation, but we can change the field value with . notation. Clearly we missed something. That “something” is the setproperty! function. Analogously to how x.field2 is parsed into getproperty(x, :field2), the expression x.field2 = "not_value_2" is parsed into setproperty!(x, :field2, "not_value_2"). This means we need to create a new method for Base.setproperty! in order to discourage others from changing the value of field2:

function Base.setproperty!(x::mutablestruct, y::Symbol, z)
    if y === :field2
        error("This field is private.")
    else
        setfield!(x, y, z)
    end
end

Now let’s test changing x.field2:

x.field2 = "really_not_value_2"

# ERROR: This field is private.

Mission accomplished. But do note that this also has its Kryptonite: the setfield! function. Similar to getfield, there’s nothing stopping a determined user from calling setfield!(x, :field2, "really_not_value_2") to do whatever they want to that field.

Technique 2: Use @var_str#

This is going to start out sounding like a tangent, but I promise it directly relates to making struct fields more “private”.

Have you ever stopped to look at the typenames of closures / anonymous functions in the Julia REPL?2

f = x -> 2x
typeof(f)

# var"#1#2"

Interesting. What is is this var"#1#2 type? We can get some help from the Julia REPL by remembering that prefixed string literals var"adsf" are actually parsed as calls to @var_str (just like r"adsf" is parsed as a call to the @r_str macro). Let’s look into it:

?@var_str

#=

search: @var_str

  var

  The syntax var"#example#" refers to a variable named Symbol("#example#"), even though #example# is not a valid Julia identifier name.

  This can be useful for interoperability with programming languages which have different rules for the construction of valid identifiers. For example, to refer to the R variable draw.segments, you can
  use var"draw.segments" in your Julia code.

  It is also used to show julia source code which has gone through macro hygiene or otherwise contains variable names which can't be parsed normally.

  Note that this syntax requires parser support so it is expanded directly by the parser rather than being implemented as a normal string macro @var_str.

  │ Julia 1.3

  │  This syntax requires at least Julia 1.3.

=#

You can read more of the same in the Julia documentation entry for var"name".

The bottom line is that the var"name" syntax can be used to create names that would otherwise be illegal in Julia. For example, despite the Julia REPL being able to autocomplete a large list of Unicode, not all of this Unicode can be parsed by the interpreter. Take for example the Double Turnstile (LaTeX: \vDash) that’s used in formal methods literature:

(args...) = all([args...])

#=

ERROR: syntax: invalid character "⊨" near column 1
Stacktrace:
 [1] top-level scope
   @ none:1

=#

It turns out this is one of the characters that Julia doesn’t parse yet3. But have no fear–var"name" syntax is here:

var"⊨"(args...) = all([args...])
var"⊨"(true, true, true)

# true

It’s a little obnoxious to type, but it gets the job done.

Using var"name" for variables works in some pretty strange places. How about doing some Orwellian math?

var"5" = 4
2+2 === var"5"

# true

Many Julia critics complain about the language having 1-based indexing instead of 0-based indexing4. Forget OffsetArrays.jl–let’s set the number 0 itself to equal 1:

x = ['a','b','c']
var"0" = 1
x[var"0"]

# "a"

Needless to say, I wouldn’t recommend putting this into production code5.

But I digress. Where were we? We want “private” fields that discourage people from touching them. How to do this? Well, consider the following code:

struct mystruct
    field1
    var"#field2"
end

Let’s create an instance of mystruct:

x = mystruct("public", "private-ish")

# mystruct("public", "privateish")

Suppose we type x. into the REPL and hit tab twice. What does the autocomplete suggest?

x. # TAB twice

# #field2  field1

OK, anyone can see the fieldnames for this struct. They can clearly access x.field1 without any problems. What happens if they try to access x.#field2?

x.#field2

#

Nothing happens except a newline. Try once or twice more and you’ll get ERROR: syntax: incomplete: premature end of input.

What’s happening here? The # character actually marks the start of comments. As soon as you type x.#, everything after the hashtag is completely ignored by Julia. The fieldname is effectively impossible to access using the form of the name printed by the autocomplete.

What if a curious user tries to query the property names of the struct?

propertynames(x)

# (:field1, Symbol("#field2"))

This gives an even more formidable looking Symbol(...) for the second fieldname. Have fun trying to get x.Symbol("#field2") to evaluate properly.

For lack of a better name, you could call this the var"#name" method. Simply use var variable names prefixed with a hashtag.

Now is this foolproof? Unfortunately, no. There are two simple ways that a determined user can access #field2:

  1. Use the var"name" syntax: x.var"#field2"

  2. Call getfield using that Symbol() form: getfield(x, Symbol("#field2"))

The bottom line is that this technique does not result in truly private fields. However there’s a few reasons why I still like the idea of var"#name" syntax for fields you don’t want other people touching:

  • Many Julia users will likely be unaware of @var_str and getfield. They will have no idea how to access the value of fields beginning with a hashtag.

  • Similar to underscores in Python, the var"#name" method communicates to advanced users that those fields shouldn’t be touched unless they know what they’re doing. It also makes the fieldname a bit more obnoxious and ugly to type, which subtly discourages them from accessing it in their code.

The only disadvantage of this method I’m aware of is having to write var"#name" everywhere in your own code. But I suspect a macro could take care of that fairly easily. I’ll leave that for another blog post someday.


1

In fact, Julia doesn’t really support private anything–variables, modules, functions….

2

You haven’t? I have. Maybe I’m just weird.

3

There are good reasons for this though, and more characters are being added in over time.

4

I have no preference between one-based and zero-based. Both have their strengths and weaknesses. I do prefer that people stop arguing though.

5

That would require a countably infinite number of variables to shift the behavior of all integer indices. You might run out of RAM.