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
or
function greet(x::PersonAndAddress)
greet(x.person)
end
and now imagine this cascaded up a few layers, so e.g. for EmployeeRecord
:
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 .
down.
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, Person
, PersonAndAddress
, 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 PersonAndAddress
or 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!
PS, I tried to ask @jeff.bezanson and @StefanKarpinski about this at Julia Day NY with admittedly limited success explaining what I meant. I hope this is a little more clear.
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”).