BinaryTraits.jl
BinaryTraits.jl copied to clipboard
Hierarchical, Contradictory Traits
Addresses !60 and introduces a test which fails on master
but passes on this branch.
The fundamental issue this PR addresses is the following:
We can apply Positive{Trait}
to an abstract type, then apply Negative{Trait}
to a subtype. We then implement a default @traitfn
for Positive{Trait}
but require a manual method implementation for the Negative{Trait}
. If we run @check
on the subtype, it can incorrectly show that the interface is satisfied.
To solve this problem, there are two things that we need to do.
- Retrieve the proper
Positive/Negative
version of the trait.
To do this, we first call traits(::Module, T::Assignable)
and traverse the type tree to find all traits which are applied to the T
in question. We now sort the tree beforehand, proceeding down the tree from Any
rather than in an unordered fashion. This allows us to determine whether the last instance of a trait applied to a type was Positive
or Negative
- Verify in the interface checker that the method which matches the type signature for the interface also matches the trait types we expect.
If we define foo(::Any) = foo(::Negative{Trait}, ::Any)
, then we require foo(::MyType)
for the Positive{Trait}
interface, the checker can give a false positive that the interface is satisfied because foo(::Any)
will pass hasmethod(foo, Tuple{MyType})
. We need to introduce a second check when hasmethod
passes which verifies that the types in the locations where the trait is substituted for the type in question that the resulting type in the method signature also possesses the trait. I call this extra check method_traits_match
This PR assumes that any given type cannot satisfy both Positive{Trait}
and Negative{Trait}
for any fixed Trait
. This is technically permissible according to BinaryTraits.jl
(assigning both to a type doesn't throw an error), so perhaps this PR is a wash from the start. However, I think this is a very useful capability. For example, it allows us to define a default interface for a type hierarchy via Negative{Trait}
, and then we can special-case subsets of the type tree which satisfy a separate interface for Positive{Trait}
.
I just learned that Negative{TraitType}
is the default trait applied to all types when we implement a new trait. With this in mind, my use-case is satisfied and I am okay closing this. However, the changes in this MR may be useful, so I'll leave it open for a maintainer to decide.
@SBuercklin I guess negative trait usages wouldn't be very common. I am curious why you need something like this. Are you able to share your use case? Your last message mentioned that your use case is already satisfied and so I'm wondering what might have changed.
It's been long enough and I'm no longer working on the project where I was trying to do this that I don't remember exactly what I was trying to accomplish. IIRC it was something along the lines of defining a default interface for an abstract type but wanting to special case the interface satisfied for certain specific subtypes. I think it was kind of an antipattern in retrospect, and it looks like I found a solution which got by without this implemented.
I don't need this nor do I think it's really a good idea at this point. I quite like the approach BinaryTraits takes to traits + interfaces, and I think this proposal was overly complicated when it comes to understanding how BT.jl works