Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hierarchical, Contradictory Traits #61

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/assignment.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@ the ones that were assigned to any supertypes of `T`.
See also [`@assign`](@ref).
"""
function traits(m::Module, T::Assignable)
traits_map = get_traits_map(m)
_traits_map = collect(pairs(get_traits_map(m)))
traits_map = sort(_traits_map; by = p -> _depth(first(p)))
base = Set{DataType}()
for (Tmap, s) in pairs(traits_map)
for (Tmap, s) in traits_map
if T <: Tmap
for trait in s
T_new = binary_trait_type(trait)
filter!(t -> !(binary_trait_type(t) <: T_new), base)
end
union!(base, s)
end
end
Expand Down
42 changes: 40 additions & 2 deletions src/interface.jl
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,12 @@ function check(m::Module, T::Assignable)
for contract in required_contracts(m, T)
tuple_type = make_tuple_type(T, contract)
method_exists = has_method(contract.func, tuple_type, contract.kwargs)
traits_match = method_exists ? method_traits_match(contract, contract.func, tuple_type) : false
return_type_matched = has_proper_return_type(contract.func, tuple_type, contract.ret)
if method_exists && return_type_matched
if method_exists && return_type_matched && traits_match
push!(implemented_contracts, contract)
else
reason = !method_exists ? "Missing implementation" : "Improper return type"
reason = !(method_exists && traits_match) ? "Missing implementation" : "Improper return type"
@warn reason contract

all_good = false
Expand Down Expand Up @@ -102,6 +103,7 @@ with the type `T` that is being checked.
"""
function make_tuple_type(T::Assignable, c::Contract)
args = [t == c.trait ? T : t for t in c.args]

return Tuple{args...}
end

Expand Down Expand Up @@ -243,3 +245,39 @@ function has_method(@nospecialize(f), @nospecialize(t), kwnames::Tuple{Vararg{Sy
return hasmethod(f, t)
end
end

"""
method_traits_match(contract, f, Tuple{argument_types...})

Checks that the method for `f` which matches the `Tuple` of types according to [has_mmethod](@ref)
is defined with types whose types actually match the relevant traits for the contract.

This is necessary because, through Julia's type hierarchy, it is possible a default method exists
but is defined using the negated trait.
"""
function method_traits_match(contract, @nospecialize(f), @nospecialize(t))
mthd = which(f, t)
m = mthd.module

# Use this to handle composite traits
composite_map = get_local_storage(m).composite_map

sig = Base.tail(fieldtypes(mthd.sig))

# Find the arguments which match the trait from the interface
idxs = findall(isequal(contract.trait), contract.args)

# Only pass if al of those types possess the correct trait.
return all(sig[idxs]) do T
_T_traits = traits(m, T)
T_traits = mapreduce(union, _T_traits; init = Set()) do _trait
if _trait in keys(composite_map)
return composite_map[_trait]
else
return Set((_trait,))
end
end

return contract.trait in T_traits
end
end
2 changes: 2 additions & 0 deletions src/trait.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export trait

abstract type BinaryTrait{T} end

binary_trait_type(::Type{<:BinaryTrait{T}}) where T = T

# This sub-module is used to keep standard prefix types
module Prefix
import ..BinaryTraits
Expand Down
11 changes: 10 additions & 1 deletion src/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ function define_const!(mod::Module, name::Symbol, val)
mod.eval(name)
end
end

# Utility to test the depth in the type tree of a given type. Root of the tree has depth 0
function _depth(::Type{T}) where {T}
if T === Any
return 0
else
return 1 + _depth(supertype(T))
end
end

# -----------------------------------------------------------------------------------------
# Storage management
# -----------------------------------------------------------------------------------------
Expand Down Expand Up @@ -169,4 +179,3 @@ function Base.isempty(st::TraitsStorage)
isempty(st.interface_map) &&
isempty(st.composite_map)
end

1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ using Test
include("test_parametric_type.jl")
include("test_cross_module.jl")
include("test_interfaces.jl")
include("test_hierarchy_interfaces.jl")
include("test_traitfn.jl")
VERSION >= v"1.1" && include("test_return_types.jl")
end
38 changes: 38 additions & 0 deletions test/test_hierarchy_interfaces.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
module HierarchicalInterfaces

using BinaryTraits
using Test

abstract type Bird end
struct Penguin <: Bird end
struct Ostrich <: Bird end

@trait Fly
@implement Negative{Fly} by fly(_)
@assign Bird with Positive{Fly}
@assign Penguin with Negative{Fly}
@assign Ostrich with Negative{Fly}

@traitfn fly(::Positive{Fly}) = "Flap!"

fly(::Ostrich) = "I'm a flightless bird!"

# Penguin should not satisfy the interface
function test_negative()
check_result = @check(Penguin)
@test check_result.result == false
end

# Ostrich does satisfy the interface
function test_positive()
check_result = @check(Ostrich)
@test check_result.result == true
end

end

using .HierarchicalInterfaces
HI = HierarchicalInterfaces

HI.test_negative()
HI.test_positive()