问题
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.
来源:https://stackoverflow.com/questions/35636248/how-can-i-write-a-trait-in-julia-with-open-ended-types