Avoiding rewriting accessor methods for new composite types with similar fields

I want to get an idea of the best practices for reusing accessor methods on new composite types with similar fields. The biggest tip for method reuse in general seems to be informal interfaces. When types have dissimilar fields, writing interface methods for each one is a necessity because they work differently. However, it’s tedious when the new types just contain other types, so all I write are accessors that do the same thing but for differently nested fields. For a double inheritance-ish example:

# in practice, types would have more fields and more accessors
struct Wheel radius end
get_wheel_radius(w::Wheel) = w.radius

struct Seat  height end
get_seat_height(s::Seat) = s.height

struct Unicycle
  wheel::Wheel
  seat::Seat
end
get_wheel_radius(u::Unicycle) = get_wheel_radius(u.wheel)
get_seat_height(u::Unicycle) = get_seat_height(u.seat)

The example with 2 accessor functions on 2 types each doesn’t look so bad, but it gets old with more accessors and types and with fields nested at different levels.
So I’m thinking that I can avoid this by carefully designing “component” types with little overlap and make my accessors work only on types containing such components on one level, like so:

# component types
struct Wheel radius end
struct Seat  height end

# types the accessors work on
struct OnlyWheel wheel::Wheel end
struct OnlySeat  seat::Seat end
struct Unicycle
  wheel::Wheel
  seat::Seat
end
get_wheel_radius(x) = x.wheel.radius
get_seat_height(x) = x.seat.height

The example is actually the same number of code lines as before, but instead of writing new accessors, I just need to write an extra containment struct Only___ for each new type.
Still, I’m not sure how good of an idea this is for a few reasons:

  • Instead of making a 15-component type a field of a derived type, I have to paste all 15 components into the derived type to use those accessors. This sacrifices clarity of what I’m deriving the type from.
  • OnlyWheel(Wheel(3.5)) is an aesthetic minus
  • stuck with naming the field for a component the same thing in all types, though the clarity isn’t too bad

I’m wondering if anyone else has tricks and tips for this specific kind of type extension.

Lazy.jl has a @forward macro that simplifies this:

3 Likes

I did read up on Lazy.@forward and ReusePatterns.@forward, but I wasn’t really sure how much they would help. I’m trying to reuse accessor methods for any number of new composite types without writing extra code for each type, but both versions of @forward seem to need to be written for each forwarded field and each type. Granted, it seems more easily written and less typo-prone than the first code example in my post.

Other than @forward I can only think of the obvious solutions:

One is

function get_wheel_radius(x :: T) where T
    if hasfield(T, :wheel)  &&  isa(getfield(x, :wheel), Wheel)
        return get_radius(x.wheel)
    else
        error("$T has no wheels")
    end
end

This hard wires field names (not good). One could probably cook up a loop that defines accessor functions for a list of fields without having to repeat the same code over and over again for each field. I doubt that is even less code than @forward.

Another option might be traits. But defining a hasWheel trait for each object also doesn’t look better than @forward.

1 Like

Why exactly are you writting getters and setters? There is a reason for that? Otherwise I would recommend defining in the interface that the objects must have a field X of type Y and just have this field. If things change, or are different internally, you can just overload Base.getproperty and Base.setproperty!.

2 Likes

I try to avoid committing to field names in most of my methods in case I decide those fields aren’t so crucial (which happens more often than I should, probably). As for getproperty, I only rarely use it to control field access because I have to put all the logic for a type in one method, whereas with several accessors, I can more easily tweak one part without affecting another.

Ok, it is an entirely reasonable reason. I have some points:

  1. Your composition may be a part of the public interface, not an implementation detail. So your vehicle should not have a get_wheel_radius but instead a get_wheel that return an object that answers to get_radius. If some attribute pertains to an inner object I am not sure if you should be creating a new getter just to access it in a single go. This, as you pointed out, is not very scalable and will lead to a lot of boilerplate code for each new level of nesting.
  2. At some point, future-proofing becomes an anti-pattern. The impression I have is that the interface is not yet solid. Is the cost of changing it after so high that you need to put everything behind a layer of indirection? It would not be best to just use fields directly, and if they disappear or change, just change the code that uses them? Encapsulation is a pattern thought for the cases the code that depends on your object is orders of magnitude larger than the code for the object itself. If this is not for a library to be widely used by the community, then this level of future-proofing/encapsulation may cause more headaches than it prevents.
  3. I fail to see a better solution than a generic get_X that always get a field named X and for which you extend specific methods for new Types that does not follow this pattern (i.e., X is computed only when asked, or maybe is in an nested object). How it could become better than that? The compiler has no way to guess what you want, it needs to be made explicit at some point.
3 Likes

Good points, the indirection probably won’t save me any time in the short or long run at a stage where nothing is set in stone yet. Nothing wrong with field access in methods on concrete types and using getters to extend these methods onto derived types; at least I know I can keep the concrete methods around.

1 Like