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.