How can I dispatch on traits relating two types, where the second type that co-satisfies the trait is uniquely determined by the first?
Asked Answered
T

1

90

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.

Teeters answered 24/2, 2016 at 22:19 Comment(13)
Whoa whoa whoa. It looks like you're trying to re-implement all of inference.jl. I don't recommend that. What do you really want to accomplish? Your example function can be written simply with a comprehension: get_measurements(similar_items) = [measurements(item) for item in similar_items].Cabalistic
Narrowly speaking, I want to be able to add the type assertion ::Array{M} after that expression given that ::X is in the method signature, and have the method callable, and have this compile, i.e. have it know what M is. But more generally, I want to be able to do arbitrary things with a symbol M that has meaning because of the satisfaction of BOTH conditions (a) the symbol X has been granted meaning from the method signature and (b) a trait [probably also in method signature] has indicated that there is a relationship between X and M (by being satisfied by {X,M}).Teeters
PS, it seems by your pointing me to inference.jl that you may be suggesting "just let the compiler figure out the type." (Maybe you're not actually suggesting that, but I'm going to respond to that suggestion just to clarify what I am trying to do.) On the contrary, I explicitly want to ensure that my code enforces certain relationships among types, which I want to express in the code by using X and a symbol (with local scope in a function, which symbol I've been calling M) that means the particular type M that, given the particular X in the signature, satisfies SomeTrait{X,M}.Teeters
So in particular (again this is not fully general, but this is implied by the fully general case) I want to be able to, say, type annotate every line with an expression that involves X and M where M had the meaning I describe above. If I had the solution I am looking for I would be at least able to do this. Does this make sense yet? I appreciate the effort to understand the question!Teeters
By the way, even though the example / use case I'm targeting appears complicated, I'm open to the idea that I'm just doing something "wrong" or "dumb," and that there is an answer of the "do something completely different and way simpler" flavor. I'd just like to see that case made, if indeed that is the right answer, since I haven't been able to come up with any solution.Teeters
I'm not familiar enough with the theory or the language internals to know, but I also have the sense that github.com/JuliaLang/julia/issues/6984#issuecomment-49751358 may be related.Teeters
The question is really long, but perhaps the technique in the answer to your other question can help here too (using Base.return_types). See #35636748 .Khosrow
I don't think so--that technique doesn't give me the ability to refer to the associated type by variable name (I can't refer to what that question calls Y in a method that dispatches on X). Additionally, If you have any suggestions for shortening the question without removing essential information, they are welcome. I believe the problem is complicated and the length reflects the complication; if I'm wrong, by all means please suggest edits to shorten it. ThanksTeeters
Is your end goal here a statically verifiable program? Accomplishing that is no small feat… and will likely be rewarded with an advanced degree instead of a few karma points. See, e.g., this thesis for static contract checking (pdf) in Haskell.Cabalistic
Based on a quick glance at your link that would be sufficient but more than necessary. (I think) what I want is much simpler: I 'know' that the type of output of some function is going to have a consistent relationship to input of that function (and it so happens that relationship is captured by Traits, but that's a detail). That reasoning is not sophisticated, so if I were a better programmer, why couldn't I represent that fact in the code itself (so to speak "make the compiler know as much as I do")?Teeters
And, yes, in turn I presume that representing the relationship inside the language would, in the right hands, cascade down both to improving performance and facilitating easier correctness checking. ATM though I really just want this b/c in phrasing the solution to the problem I'm trying to model, I find myself consistently structuring solutions in terms of 'any X,M satisfying [conditions on X,M]' but my "atoms" are all Xs, i.e. the things I actually carry around or pass to functions are Xs, whereas the Ms take place under the hood / are invisible to the caller passing the Xs.Teeters
in the time since this question was asked, the Traits package has been discontinued, picked up a year later by someone else, and discontinued again (and that was three years ago)Obstruct
Also, the packages which fulfill a similar niche (SimpleTraits, WhereTraits, CanonicalTraits) don't implement constraints, so this question's code does not run, and cannot even be made to run anymore. Seems like it should be closed no?Obstruct
S
1

what about parameterized functions? instead of trying to define a relationship between types you can use parameterized functions to specify the type relationships. you can define a function like get_measurements{T}(similar_items::Array{T}) and specify the expected output type for each T

Sidwel answered 6/7, 2023 at 19:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.