[ANN] PrivateFields.jl

GitHub link: GitHub - SBuercklin/PrivateFields.jl: No touching! (should land on the General Registry in ~3 days from posting)

Overview

PrivateFields.jl implements the @private_struct, @private_method macros.

@private_struct lets you tag fields of structs as @private, in which case those fields cannot be accessed through the traditional dot-syntax a.b.

The exception to this behavior is in methods tagged with @private_method and a special syntax to specify which arguments should be allowed to have privileged access to the private fields within that method. A simple demo can be found in the Example section.

Motivation

A common pattern in Julia packages is to build a core package which defines interfaces in library packages, typically with some core types supporting that interface. Users then interact with these types, ideally through the defined interface. However, Julia also allows users to interact directly with the fields of these types through the dot syntax. This usage can lead to users relying on implementation details of types, such as particular fieldnames, rather than the function interfaces to these types.

PrivateFields.jl aims to make interacting with the types through the convenient dot-syntax less ergonomic, encouraging the use of a preferred API rather than implicit interfaces through fieldnames (e.g. getter/setter methods).

At the same time, the dot-syntax does serve a purpose: developers of the original type could use the dot syntax for implementing, maintaining, and testing these interfaces, as they are responsible for the internal layout of the struct (fieldnames, order of fields, etc). The @private_method macro lets developers implement code which temporarily permits the dot syntax within the scope of that method for select arguments.

My goal is that by encouraging the use of function interfaces, it becomes easier to shrink the API that needs to be supported for inter-project compatibility. Developers should feel less constrained by how their types are defined, and users should feel more comfortable that new releases continue to be backwards compatible through the interfaces they use.

Example

# @private denotes the private fields
@private_struct struct Foo{X,Y}
    @private x::X
    y::Y
end

foo1 = Foo(3.0, 4.0)
foo2 = Foo(5, 6)

# 4-colon syntax denotes that only f1 should allow private access to fields
@private_method f(f1::::Foo, f2::Foo) = f1.x + f2.y
g(f1::Foo, f2::Foo) = f1.x + f2.y

f(foo1, foo2) # 9.0

g(foo1, foo2) # ERROR: PrivacyError: Attempted to directly access private field Foo.x outside private context

Remaining Work

Fully General struct Support

Right now only immutable types are supported, mostly as a proof of concept. Mutable types should be implemented as well, which will need their own implementation on the setproperty! side.

Settle on the “correct” approach

This approach works well if you don’t want a custom getproperty implementation. If you do want a custom getproperty on top of private fields, this package requires you to implement PrivateFields.getproperty_direct instead.

I had a couple other ideas for how we toggle the @private_method mode, but I’m open to input on a better way to structure a solution to this.

Modes of Operation

At startup, it might be helpful to set an environment variable which enables/disables this behavior, possibly on a per-project basis.

Restrict where @private_method can be used

Right now, there’s nothing stopping downstream users from tagging their code with @private_method and continuing to use the dot syntax. One thought I had was to only permit @private_method in the same module where the corresponding types are defined. This enforces that users cannot directly access fields with the dot syntax, while still allowing developers to access the internals.

Extra Thoughts

Why Not Just Use getproperty?

Maintaining getproperty implementations is cumbersome, especially if you’re changing the fields you want to store in the underlying struct. I find it’s easier to maintain the individual methods that perform a calculation to retrieve a value than a list of if-elseif statements where I check the symbol to implement how a value should be computed.

On Specifying Interfaces

The problem which accompanies this is actually defining interfaces. I like BinaryTraits.jl for not only defining, but also testing my interfaces in library packages. The testing performed by BinaryTraits.jl isn’t over correctness, but merely whether the proper methods have been implemented for a type to satisfy all attached interfaces.

I developed this package with BinaryTraits.jl in mind for enforcing the getter methods (and later, setter! methods) that define the interface. In other words, BinaryTraits.jl tells you what you need to implement, and PrivateFields.jl keeps you honest about obeying those interfaces.

6 Likes