How can I write a Trait in Julia with open-ended t

2019-06-19 01:56发布

问题:

This is an attempt to simplify one part of the question I asked here:

I want to write some code that is guaranteed to work on types that meet certain criteria. Let's say today I write some code:

immutable Example
    whatever::ASCIIString
end
function step_one(x::Example)
    length(x.whatever)
end
function step_two(x::Int64)
    (x * 2.5)::Float64
end
function combine_two_steps{X}(x::X)
    middle = step_one(x)
    result = step_two(middle)
    result
end
x = Example("Hi!")
combine_two_steps(x)

Running this works:

julia> x = Example("Hi!")
Example("Hi!")

julia> combine_two_steps(x)
7.5

Then another day I write some more code:

immutable TotallyDifferentExample
    whatever::Bool
end
function step_one(x::TotallyDifferentExample)
    if x.whatever
        "Hurray"
    else
        "Boo"
    end
end
function step_two(x::ASCIIString)
    (Int64(Char(x[end])) * 1.5)::Float64
end

And what do you know, my generic combine function still works!

julia> y = TotallyDifferentExample(false)
TotallyDifferentExample(false)

julia> combine_two_steps(y)
166.5

Hurray! But, say it's a late night and I'm trying to do this AGAIN on a third example. I remember to implement step_one, but I forget to implement step_two!

immutable ForgetfulExample
    whatever::Float64
end
function step_one(x::ForgetfulExample)
    x.whatever+1.0
end

Now when I run this, I'm going to get a run-time error!

julia> z = ForgetfulExample(1.0)
ForgetfulExample(1.0)

julia> combine_two_steps(z)
ERROR: MethodError: `step_two` has no method matching step_two(::Float64)

Now, I work for a manager who will KILL ME if I ever get a run-time error. So what I need to do to save my life is to write a Trait that essentially says "if the type implements this trait, then it's safe to call combine_two_steps."

I want to write something like

using Traits
@traitdef ImplementsBothSteps{X} begin
    step_one(X) -> Y
    step_two(Y) -> Float64
end
function combine_two_steps{X;ImplementsBothSteps{X}}(x::X)
    middle = step_one(x)
    result = step_two(middle)
    result
end

b/c then I'd know that if combine_two_steps is ever dispatched, then it will run without raising an error that these methods don't exist.

Equivalently, istrait(ImplementsBothSteps{X}) (being true) is equivalent to combine_two_steps will run without error-from-nonexistence-of-required-methods.

But, as everybody knows, I can't use that trait definition, because Y has no meaning. (In fact, oddly enough the code compiles without error,

julia> @traitdef ImplementsBothSteps{X} begin
           step_one(X) -> Y
           step_two(Y) -> Float64
       end

julia> immutable Example
           whatever::ASCIIString
       end

julia> function step_one(x::Example)
           length(x.whatever)::Int64
       end
step_one (generic function with 1 method)

julia> function step_two(x::Int64)
           (x * 2.5)::Float64
       end
step_two (generic function with 1 method)

julia> istrait(ImplementsBothSteps{Example})
false

but the types don't satisfy the trait even though the methods exist for some Y.) My first thought is I can change Y to something like Any

using Traits
@traitdef ImplementsBothSteps{X} begin
    step_one(X) -> Any
    step_two(Any) -> Float64
end

but this fails too b/c the Any really is supposed to be something like Some, not literally the Any type (since I never implemented a method step_two that could take any type as input), but some particular type that's shared across both lines!

So, the question is: what would you do in this situation? You want to pass around a "spec" (here in the form of the contract expressed by the Trait) such that any programmer anywhere who meets the spec is guaranteed to be able to use your function combine_two_steps, but the spec essentially has an existential quantifier in its definition.

Is there a workaround? A better approach to writing the "spec" (e.g. "Don't use Traits, use something else"?) Etc.

By the way, it may sound contrived, but the above-linked question and this question are coming up regularly in a project I'm working on. I'm essentially stuck at a roadblock caused by this problem and have ugly workarounds that work case-by-case, but no approach to the general case.

回答1:

Is this satisfactory:

@traitdef ImplementsStep2{Y} begin
    step_two(Y) -> Float64
end

# consider replacing `any` with `all`
@traitdef AnotherImplementsBothSteps{X} begin
    step_one(X)
    @constraints begin
        any([istrait(ImplementsStep2{Y}) for Y in Base.return_types(step_one,(X,))])
    end
end

With these trait definitions we have:

julia> istrait(ImplementsStep2{Int64})
true

julia> istrait(AnotherImplementsBothSteps{Example})
true

The trick is to use @constraints to basically do the non-straightforward stuff. And to use Base.return_types to get at the return types for a method. Admittedly this is a bit of a hack, but this is what my digging came up with. Perhaps a future version of Traits.jl will have better tools for this.

I've used any in the trait definition. This is a bit lax. Using all may be stricter but represent the constraint better, depending on what level of compile-time checking is desired.

Of course, Julia's good introspection and try ... catch allows doing all this checking at run-time.



回答2:

Generalizing on the suggestion in my question of using Any actually can work also, although it's ugly and doesn't really get to the point. Suppose you have already implemented methods

step_one(X) -> Y
step_two(Y) -> Z

Then you can write the trait as

@traitdef implements_both_steps begin
    step_one(X) -> Any
    step_two(Any) -> Z
end

And just add a dummy method

function step_two(x::Any)
    typeof(x)==Y ? step_two(x::Y) : error("Invalid type")
end

This can be wrapped up in a macro as well to save on repeating the pattern, and then once that method is implemented the trait is satisfied. It's a hack that I've been using (and that works) b/c it's fairly straightforward, but the solution is not in the spirit of my question.