The general idea is that I often have types that are composed of smaller “pieces” of the types. Not subtypes in the sense of a type tree, rather partial-types in the sense of records with only a fraction of the next type’s information.
For example suppose I were trying to “build” an EmployeeRecord. I might proceed as follows. Note, this is very contrived, but in the actual use case (which is too complicated to transform into a MWE) there’s a better reason for building up the big combined types from the smaller partial types.
immutable Person name::String social::Int64 end immutable Address number::Int64 street::String city::String state::String end immutable PersonAndAddress person::Person address::Address end immutable EmployeeRole role::String seniority_level::Int64 end immutable EmployeeRecord person_and_address::PersonAndAddress role::EmployeeRole years_at_company::Int64 salary::Float64 end
Now suppose I have some function that operates on the
name field of
Person. Again, this is contrived, but bear with me:
function greet(x::Person) println("Hi, $(x.name)") end
But I have other functions that also need to make use of the
greet capability, yet they might not be operating on
Person, but on one of the more built-up types. So say there’s some function like
function writeletter(x::PersonAndAddress) greet(x) println("How are you? Sincerely, Julia") mail_stdout_to(x.address) end
Then I need to provide some wrapper function, either
function greet(x::PersonAndAddress) println("Hi, $(x.person.name)") end
function greet(x::PersonAndAddress) greet(x.person) end
and now imagine this cascaded up a few layers, so e.g. for
function greet(x::EmployeeRecord) greet(x.person_and_address) #assuming the greet above was provided end
and so on for even bigger smashings-together of types.
Hopefully the problem is clear by now: as the combinations of types get bigger and bigger, the amount of overhead to keep the methods all in order gets insane.
But there’s a good reason for having the distinct partial-types, namely that I can write generic methods that only need the information contained in those partial-types, and also in the case of my example the lower-down partial-types tend to have information that helps build the higher-up partial types.
In Java or another less elegant language, I might model this situation with something like a chain of abstract types with fields with methods coded against the abstract types and then a corresponding concrete type for each level of abstraction. That way the additional fields even at the most built-up type would have the fields of the lowest-down partial type accessible at the same “level,” i.e. without having to dig deeper into the record-of-record-of-records to get at the underlying data…everything would always be one
It seems I might use Traits/interfaces where each type above “has-a”
name, though I’m not sure how I can elegantly or concisely implement
get_name for each consecutive wrapper type. (The abstract field with types solution seems like it would handle that though.)
Is there a way to do this in Julia? Possibly with some macros? All I’ve come up with so far is a sketch of a macro that would essentially examine the
fieldnames of two types, construct code that defines a new type that contains all the field names (I’m not worried about clashes at the moment; I can code around that), and then define the type and then make an object of that type, but it seems dangerous to me to build a codebase atop using macros to define types on the fly that I haven’t actually explicitly specified. That is, since I’m coding methods targeted to many levels of built-up-ness, I’d much prefer to write explicitly here are the various levels (in the example above,
EmployeeRecord, etc.), but to be able to count on the idea that when I write
x.y in a method (or even a more generic
get_from(x,y) or something) that when a future
x wraps the
x the method actually expected in some number of layers of nesting, it will “just work.”
Note, in the example above, the graph of partially-built-up types has more than one root following “de-nesting.” For example you could de-nest
EmployeeRecord into either
EmployeeRole and then those into separate composite types or primitives. But I would also be interested in an answer that only worked on trees with a single de-nesting path, e.g. where for every type T_N, the pattern looked something like
immutable T_NPlusOne nested::T additional_info::Float64OrSomeOtherPrimitive end immutable T_NPlusTwo nested::T_NPlusOne more_additiona_info::Float64OrSomeOtherPrimitive end
, on the off-chance the original question is too difficult but that restriction is an answerable sub-goal of the original question.
Is there a trick to this? Or a design pattern that I can follow that achieves effectively the same thing? Thanks!
PPS This question is loosely related to ideas in the NamedTuples.jl package, to Traits in general including this and this, and to this question about coupling types (although my meaning of “combine” is not that similar to @kellertuer’s usage of “couple”).