English 中文(简体)
How can I dispatch on traits relating two types, where the second type that co-satisfies the trait is uniquely determined by the first?
原标题:
  • 时间:2016-02-24 22:19:27
  •  标签:
  • julia
  • traits

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

  1. 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 and M, and
  2. 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

  1. Use Traits.jl
  2. Do something pretty similar to what you ve done so far
  3. Also do some clever thing that the answerer will indicate,

but I m open to the idea that in fact, the correct answer is

  1. Abandon this approach, you re thinking about the problem the wrong way
  2. Instead, think about it this way: MWE

I d also be perfectly satisfied by answers to the form

  1. 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

  1. 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.

问题回答

Certainly! Here s the complete code incorporating the approaches discussed:

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
end

type Rope
    color::String
    age_in_years::Float64
    strength::Float64
    length::Float64
end

type Paper
    color::String
    age_in_years::Int64
    content::String
    width::Float64
    height::Float64
end

function isnew(x::Rope)
    x.age_in_years < 10.0
end

function coolness(x::Rope)
    if x.color == "Orange"
        2.0
    elseif x.color != "Taupe"
        1.0
    else
        0.0
    end
end

function isnew(x::Paper)
    x.age_in_years < 1.0
end

function coolness(x::Paper)
    x.content == "StackOverflow Answers" ? 1000.0 : 0.0
end

function measurements(x::Rope)
    x.length
end

function measurements(x::Paper)
    (x.height, x.width)
end

function get_measurement_type_of_product(::Type{Rope})
    Float64
end

function get_measurement_type_of_product(::Type{Paper})
    Tuple{Float64, Float64}
end

@traitfn function get_measurements{X}(similar_items::Array{X, 1}) where {X, M}
    @traitconstraint IsProductWithMeasurement{X, M}
    all_measurements = similar_items |> map(x -> measurements(x)::M) |> collect
    all_measurements::Array{M, 1}
end

rope1 = Rope("blue", 1.0, 1.0, 1.0)
rope2 = Rope("red", 2.0, 2.0, 2.0)
paper = Paper("white", 0, "StackOverflow Answers", 8.5, 11.0)

rope_measurements = get_measurements([rope1, rope2])
paper_measurements = get_measurements([paper])

println(rope_measurements)
println(paper_measurements)

In this code, we define the traits IsProduct and IsProductWithMeasurement using the Traits.jl package. Then, we define the Rope and Paper types along with their associated functions.

To address the issue of type inference and dispatching on associated types, we introduce the get_measurement_type_of_product function. This function determines the associated type for a given base type.

Finally, we define the get_measurements function, which uses the measurements function and the associated type M to calculate the measurements of the products in the array. The @traitconstraint macro is used to enforce that X satisfies the IsProductWithMeasurement trait.

We create instances of Rope, Paper, and call get_measurements with these instances to demonstrate the usage.

Note: This code assumes that you have already installed the Traits.jl package by running ] add Traits in the Julia REPL.





相关问题
How do you return an Iterator in Scala?

What must I do in order to be able to return an Iterator from a method/class ? How would one add that trait to a class?

boost add_reference not working with template parameter

I am trying to use type traits to add reference to a template parameter. template < class T > struct S { typename add_reference< T >::type reference; // reference member should always be ...

Traits and abstract methods override in Scala

I have a base abstract class (trait). It has an abstract method foo(). It is extended and implemented by several derived classes. I want to create a trait that can be mixed into the derived classes so ...

scala: mixins depending on type of arguments

I have a set of classes of models, and a set of algorithms that can be run on the models. Not all classes of models can perform all algorithms. I want model classes to be able to declare what ...

热门标签