thautwarm/CanonicalTraits.jl

hasmethod error

aminya opened this issue · 7 comments

I am trying to define this trait but it gives me errors.

@trait StringConvertible{T} begin
    (hasmethod) :: (convert, Tuple{String, T}) => true
end
ERROR: error in method definition: function Base.hasmethod must be explicitly imported to be extended
Stacktrace:
 [1] top-level scope at none:0
 [2] top-level scope at none:1

Trying the following changes the error:

@trait StringConvertible{T} begin
    (Base.hasmethod) :: (convert, Tuple{String, T}) => true     
end
ERROR: LoadError: MethodError: no method matching extract_method_interface(::Expr, ::Array{Expr,1}, ::Bool, ::Array{Any,1})
Closest candidates are:
  extract_method_interface(::Symbol, ::AbstractArray, ::Any, ::AbstractArray) at C:\Users\yahyaaba\.julia\packages\CanonicalTraits\IA6yQ\src\Utils.jl:87
Stacktrace:
 [1] ##CanonicalTraits 146#400#51 at .\none:0 [inlined]
 [2] ##CanonicalTraits 144#398#50 at .\none:0 [inlined]
 [3] ##CanonicalTraits 137#391#49 at .\none:0 [inlined]
 [4] ##CanonicalTraits 135#389#48 at .\none:0 [inlined]
 [5] extract_method(::Expr) at C:\Users\yahyaaba\.julia\packages\CanonicalTraits\IA6yQ\src\Utils.jl:99
 [6] iterate at .\generator.jl:47 [inlined]
 [7] collect_to!(::Array{LineNumberNode,1}, ::Base.Generator{SubArray{Any,1,Array{Any,1},Tuple{UnitRange{Int64}},true},typeof(CanonicalTraits.extract_method)}, ::Int64, ::Tuple{Base.OneTo{Int64},Int64}) at .\array.jl:711
 [8] collect_to_with_first!(::Array{LineNumberNode,1}, ::LineNumberNode, ::Base.Generator{SubArray{Any,1,Array{Any,1},Tuple{UnitRange{Int64}},true},typeof(CanonicalTraits.extract_method)}, ::Tuple{Base.OneTo{Int64},Int64}) at .\array.jl:689
 [9] _collect(::SubArray{Any,1,Array{Any,1},Tuple{UnitRange{Int64}},true}, ::Base.Generator{SubArray{Any,1,Array{Any,1},Tuple{UnitRange{Int64}},true},typeof(CanonicalTraits.extract_method)}, ::Base.EltypeUnknown, 
::Base.HasShape{1}) at .\array.jl:683
 [10] collect_similar at .\array.jl:607 [inlined]
 [11] map at .\abstractarray.jl:2072 [inlined]
 [12] ##CanonicalTraits 406#660#110 at C:\Users\yahyaaba\.julia\packages\CanonicalTraits\IA6yQ\src\Typeclasses.jl:101 [inlined]
 [13] (::CanonicalTraits.var"##CanonicalTraits 406#660#110"{Symbol,SubArray{Any,1,Array{Any,1},Tuple{UnitRange{Int64}},true},LineNumberNode,Array{Pair{Symbol,Expr},1},Array{Expr,1}})(::Array{Any,1}) at .\none:0   
 [14] ##CanonicalTraits 404#658#109 at .\none:0 [inlined]
 [15] ##CanonicalTraits 416#670#108 at C:\Users\yahyaaba\.julia\packages\CanonicalTraits\IA6yQ\src\Typeclasses.jl:96 [inlined]
 [16] (::CanonicalTraits.var"##CanonicalTraits 414#668#107"{LineNumberNode,Expr,Array{Pair{Symbol,Expr},1},Array{Expr,1}})(::Expr) at .\none:0 [17] trait(::LineNumberNode, ::Any, ::Any) at C:\Users\yahyaaba\.julia\packages\CanonicalTraits\IA6yQ\src\Typeclasses.jl:96
 [18] @trait(::LineNumberNode, ::Module, ::Any, ::Any) at C:\Users\yahyaaba\.julia\packages\CanonicalTraits\IA6yQ\src\Typeclasses.jl:151      
in expression starting at none:1

Hello, this is intentional because overloading any existing julia function can cause unexpected behaviour.
CanonicalTraits will take over some methods definition functionalities overlapped with mulitiple dispatch.
Although you can try import Base: hasmethod, it's still not encouraged if you don't understand the underlying of CanonicalTraits.
Besides, the definition of multiple-ary trait method should be

@trait StringConvertible{T} begin
    yourhasmethod :: [convert, Tuple{String, T}] => true
end

[a, b] instead of (a, b).

I don't want to overload hasmethod. I just want to define a trait that works based on what hasmethod gives.

@aminya this is what you want:

 @trait StringConvertible{T} begin
   has_tostr_method :: Type{T} => Bool
   has_tostr_method(x) = hasmethod(convert, Tuple{Type{String}, x}) # default method
 end

@implement StringConvertible{T} where T # implement for any types, it'll use default method
Base.convert(::Type{String}, x::Symbol) = string(x)

julia> has_tostr_method(Symbol)
true

julia> has_tostr_method(Int)
false

Thanks! This works. It would be cool to add this example to the documentation. This is one of the most used cases for traits.

However, this seems slower than the original hasmethod.

julia> t = Int64

julia> @btime  has_tostr_method($t)
  21.700 μs (20 allocations: 864 bytes)

julia> @btime  hasmethod(convert, Tuple{String, $t})
  13.499 μs (2 allocations: 64 bytes)
false

Thanks! This works. It would be cool to add this example to the documentation. This is one of the most used cases for traits.

Welcome! I'm always not sure which part people would get interested in, and if you want maybe you could add a PR to register this example in README or documentations.

Although this seems slower than the original hasmethod.

Yes, in some cases it can be, and this is one example.

When performance hit occurs, it is usually really really confusing to me, because the generated code is actually equivalent and CanonicalTriats shall add no overhead:

julia> @code_llvm has_tostr_method(t)

;  @ none within `has_tostr_method'
; Function Attrs: uwtable
define i8 @julia_has_tostr_method_17849(%jl_value_t addrspace(10)*) #0 {
top:
; ┌ @ REPL[11]:2 within `##has_tostr_method#277#6'
; │┌ @ reflection.jl:1245 within `hasmethod'
; ││┌ @ reflection.jl:1247 within `#hasmethod#25'
     %1 = call %jl_value_t addrspace(10)* inttoptr (i64 1720400272 to %jl_value_t addrspace(10)* (%jl_value_t addrspace(10)*, i64)*)(%jl_value_t addrspace(10)* addrspacecast (%jl_value_t* inttoptr (i64 852995920 to %jl_value_t*) to %jl_value_t addrspace(10)*), i64 -1)
     %2 = addrspacecast %jl_value_t addrspace(10)* %1 to %jl_value_t addrspace(12)*
     %3 = icmp ne %jl_value_t addrspace(12)* %2, addrspacecast (%jl_value_t* inttoptr (i64 162493776 to %jl_value_t*) to %jl_value_t addrspace(12)*)
     %4 = zext i1 %3 to i8
; └└└
  ret i8 %4
}

julia> @code_llvm  hasmethod(convert, Tuple{String, t})

;  @ reflection.jl:1245 within `hasmethod'
; Function Attrs: uwtable
define i8 @julia_hasmethod_17851(%jl_value_t addrspace(10)*) #0 {
top:
; ┌ @ reflection.jl:1247 within `#hasmethod#25'
   %1 = call %jl_value_t addrspace(10)* inttoptr (i64 1720400272 to %jl_value_t addrspace(10)* (%jl_value_t addrspace(10)*, i64)*)(%jl_value_t addrspace(10)* addrspacecast (%jl_value_t* inttoptr (i64 368565152 to %jl_value_t*) to %jl_value_t addrspace(10)*), i64 -1)
   %2 = addrspacecast %jl_value_t addrspace(10)* %1 to %jl_value_t addrspace(12)*
   %3 = icmp ne %jl_value_t addrspace(12)* %2, addrspacecast (%jl_value_t* inttoptr (i64 162493776 to %jl_value_t*) to %jl_value_t addrspace(12)*)
   %4 = zext i1 %3 to i8
; └
  ret i8 %4
}

I don't think adding more source code position metadata will affect the performance..

I thought CanonicalTraits.jl is able to do something similar to what Tricks.jl does. Because for Tricks, static_hasmethod takes almost 0 time.

@aminya I know that trick, and even without the julia edge system.
If your program is much slower than getting the world age, you can use this trick.
I did post an implementation in Julia CN discourse: https://discourse.juliacn.com/t/topic/2347

This trick is also applied to CanonicalTraits.jl, in @implement!, for constant method lookup. However Base.hasmethod itself is not a CanonicalTraits thing so it wouldn't be handled that way, because I cannot know an outer function is pure or not.