Say I have a Julia trait that relates to two types: one type is a sort of "base" type that may satisfy a sort of partial trait, and the other is an associated type that is uniquely determined by the base type. (That is, the relation from BaseType -> AssociatedType is a function.) Together, these types satisfy a composite trait that is one of interest to me.
For example:
using Traits
@traitdef IsProduct{X} begin
isnew(X) -> Bool
coolness(X) -> Float64
end
@traitdef IsProductWithMeasurement{X,M} begin
@constraints begin
istrait(IsProduct{X})
end
measurements(X) -> M
#Maybe some other stuff that dispatches on (X,M), e.g.
#fits_in(X,M) -> Bool
#how_many_fit_in(X,M) -> Int64
#But I don t want to implement these now
end
Now here are a couple of example types. Please ignore the particulars of the examples; they are just meant as MWEs and there is nothing relevant in the details:
type Rope
color::ASCIIString
age_in_years::Float64
strength::Float64
length::Float64
end
type Paper
color::ASCIIString
age_in_years::Int64
content::ASCIIString
width::Float64
height::Float64
end
function isnew(x::Rope)
(x.age_in_years < 10.0)::Bool
end
function coolness(x::Rope)
if x.color=="Orange"
return 2.0::Float64
elseif x.color!="Taupe"
return 1.0::Float64
else
return 0.0::Float64
end
end
function isnew(x::Paper)
(x.age_in_years < 1.0)::Bool
end
function coolness(x::Paper)
(x.content=="StackOverflow Answers" ? 1000.0 : 0.0)::Float64
end
Since I ve defined these functions, I can do
@assert istrait(IsProduct{Rope})
@assert istrait(IsProduct{Paper})
And now if I define
function measurements(x::Rope)
(x.length)::Float64
end
function measurements(x::Paper)
(x.height,x.width)::Tuple{Float64,Float64}
end
Then I can do
@assert istrait(IsProductWithMeasurement{Rope,Float64})
@assert istrait(IsProductWithMeasurement{Paper,Tuple{Float64,Float64}})
So far so good; these run without error. Now, what I want to do is write a function like the following:
@traitfn function get_measurements{X,M;IsProductWithMeasurement{X,M}}(similar_items::Array{X,1})
all_measurements = Array{M,1}(length(similar_items))
for i in eachindex(similar_items)
all_measurements[i] = measurements(similar_items[i])::M
end
all_measurements::Array{M,1}
end
Generically, this function is meant to be an example of "I want to use the fact that I, as the programmer, know that BaseType
is always associated with AssociatedType
to help the compiler with type inference. I know that whenever I do a certain task [in this case, get_measurements
, but generically this could work in a bunch of cases] then I want the compiler to infer the output type of that function in a consistently patterned way."
That is, e.g.
do_something_that_makes_arrays_of_assoc_type(x::BaseType)
will always spit out Array{AssociatedType}
, and
do_something_that_makes_tuples(x::BaseType)
will always spit out Tuple{Int64,BaseType,AssociatedType}
.
AND, one such relationship holds for all pairs of <BaseType,AssociatedType>
; e.g. if BatmanType
is the base type to which RobinType
is associated, and SupermanType
is the base type to which LexLutherType
is always associated, then
do_something_that_makes_tuple(x::BatManType)
will always output Tuple{Int64,BatmanType,RobinType}
, and
do_something_that_makes_tuple(x::SuperManType)
will always output Tuple{Int64,SupermanType,LexLutherType}
.
So, I understand this relationship, and I want the compiler to understand it for the sake of speed.
Now, back to the function example. If this makes sense, you will have realized that while the function definition I gave as an example is correct in the sense that it satisfies this relationship and does compile, it is un-callable because the compiler doesn t understand the relationship between X
and M
, even though I do. In particular, since M
doesn t appear in the method signature, there is no way for Julia to dispatch on the function.
So far, the only thing I have thought to do to solve this problem is to create a sort of workaround where I "compute" the associated type on the fly, and I can still use method dispatch to do this computation. Consider:
function get_measurement_type_of_product(x::Rope)
Float64
end
function get_measurement_type_of_product(x::Paper)
Tuple{Float64,Float64}
end
@traitfn function get_measurements{X;IsProduct{X}}(similar_items::Array{X,1})
M = get_measurement_type_of_product(similar_items[1]::X)
all_measurements = Array{M,1}(length(similar_items))
for i in eachindex(similar_items)
all_measurements[i] = measurements(similar_items[i])::M
end
all_measurements::Array{M,1}
end
Then indeed this compiles and is callable:
julia> get_measurements(Array{Rope,1}([Rope("blue",1.0,1.0,1.0),Rope("red",2.0,2.0,2.0)]))
2-element Array{Float64,1}:
1.0
2.0
But this is not ideal, because (a) I have to redefine this map each time, even though I feel as though I already told the compiler about the relationship between X
and M
by making them satisfy the trait, and (b) as far as I can guess--maybe this is wrong; I don t have direct evidence for this--the compiler won t necessarily be able to optimize as well as I want, since the relationship between X
and M
is "hidden" inside the return value of the function call.
One last thought: if I had the ability, what I would ideally do is something like this:
@traitdef IsProduct{X} begin
isnew(X) -> Bool
coolness(X) -> Float64
∃ ! M s.t. measurements(X) -> M
end
and then have some way of referring to the type that uniquely witnesses the existence relationship, so e.g.
@traitfn function get_measurements{X;IsProduct{X},IsWitnessType{IsProduct{X},M}}(similar_items::Array{X,1})
all_measurements = Array{M,1}(length(similar_items))
for i in eachindex(similar_items)
all_measurements[i] = measurements(similar_items[i])::M
end
all_measurements::Array{M,1}
end
because this would be somehow dispatchable.
So: what is my specific question? I am asking, given that you presumably by this point understand that my goals are
- Have my code exhibit this sort of structure generically, so that
I can effectively repeat this design pattern across a lot of cases
and then program in the abstract at the high level of
X
andM
, and - do (1) in such a way that the compiler can still optimize to the best of its ability / is as aware of the relationship among types as I, the coder, am
then, how should I do this? I think the answer is
- Use
Traits.jl
- Do something pretty similar to what you ve done so far
- Also do some clever thing that the answerer will indicate,
but I m open to the idea that in fact, the correct answer is
- Abandon this approach, you re thinking about the problem the wrong way
- Instead, think about it this way: MWE
I d also be perfectly satisfied by answers to the form
- What you are asking for is a "sophisticated" feature of Julia that is still under development, and is expected to be included in v0.x.y, so just wait...
and I m less enthusiastic about (but still curious to hear) an answer such as
- Abandon Julia; instead, use the language ________ that is designed for this type of thing
I also think this might be related to the question of typing Julia s function outputs, which as I take it is also under consideration, though I haven t been able to puzzle out the exact representation of this problem in terms of that one.