tk3369 / BinaryTraits.jl

Can do or not? It's easy. See https://tk3369.github.io/BinaryTraits.jl/dev/
MIT License
51 stars 3 forks source link

@implement requires explicit argument types #25

Closed KlausC closed 4 years ago

KlausC commented 4 years ago

The argument requires to specify an argument type for each argument - could default to Any

Yes, but I am unsure if Any is the right default. When I tried to use it with the AbstractArray interface, which uses duck typing in the interface, I figured that I need to use Base.Bottom instead. See the LinearIndexing trait example. If we want to impose a default then I think Bottom is more appropriate because otherwise the implementer must define the function that accepts explicitly Any.

KlausC commented 4 years ago

But defining foo(x::Any) is equivalent to foo(x), so I don't see the point. Can you show me the exact issues with AbstractArray - IIRC that is an abstract base type, not an interface in the sense otf this package. At the other hand defining foo(x::Base.Bottom) does not make sense, because there exists no x, which would trigger its calling.

julia> foo(x::Base.Bottom) = 0
foo (generic function with 1 method)

julia> foo(1)
ERROR: MethodError: no method matching foo(::Int64)
Closest candidates are:
  foo(::Union{}) at REPL[65]:1
Stacktrace:
 [1] top-level scope at REPL[66]:1
 [2] eval(::Module, ::Any) at ./boot.jl:331
 [3] eval_user_input(::Any, ::REPL.REPLBackend) at /home/julia/julia/usr/share/julia/stdlib/v1.5/REPL/src/REPL.jl:132
 [4] run_backend(::REPL.REPLBackend) at /home/crusius/.julia/dev/Revise/src/Revise.jl:1070
 [5] top-level scope at none:1

julia> methods(foo)
# 1 method for generic function "foo":
[1] foo(x::Union{}) in Main at REPL[65]:1

julia> foo(x) = 1
foo (generic function with 2 methods)

julia> methods(foo)
# 2 methods for generic function "foo":
[1] foo(x::Union{}) in Main at REPL[65]:1
[2] foo(x) in Main at REPL[68]:1

julia> foo(x::Any) = 2
foo (generic function with 2 methods)

julia> methods(foo)
# 2 methods for generic function "foo":
[1] foo(x::Union{}) in Main at REPL[65]:1
[2] foo(x) in Main at REPL[70]:1

julia> foo(99)
2
tk3369 commented 4 years ago

Let's try a more concrete example.

@trait Friendly prefix Is,Not
@implement IsFriendly by play_with(other::Any)

Now, let's say you have two animal types:

struct Cat end
struct Mouse end

And, we believe that Cat is friendly:

@assign Cat with Friendly

If we would have to implement the play function for cat, then we do:

play(me::Cat, other::Cat) = "little scratchy"

This implementation would not satisfy the interface because it does not take anything other than Cat. The interface says that your method MUST accept Any (not a subclass of Any). Hence, you must provide an implementation that can cover the type that is being specified. To satisfy this contract, we have to do the following:

play(me::Cat, other::Any) = "little scratchy"

And that would be a bad choice because the other argument could really be anything, not just animals.

More formally, functions signatures are contra-variant in this case, which is inverted from the case of calling a function. I wrote about the variance topic in my book Hands-on Design Patterns and Best Practices with Julia. Another good reference is Eli's blog about covariance and contravariance.

KlausC commented 4 years ago

I am quite aware of the covariance-contravariance subject (I worked a while with Scala). Neverthless I cannot understand your conclusion, that the default value should be Bottom and not Any. Using Any is exactly what the method definition does in the absence of an explicit argument type.

Maybe I am confused about what is the purpose of @implement and interfaces. Currently I see only the check function, which verifies with hasmethod if a method with exactly the given argument types is defined. With other words, the check actually done is invariant.

What I would expect, would be like this: If I specify @implement IsFriendly by play_with(other::Mammal), then I can call play_with(me, other) if only other isa Mammal. But I do not expect necessarily an implementation play_with(..., ::Mammal); there could be play_with(..., ::Animal) with Cat <: Mammal <: Animal, which is currently not recognized by check.

Maybe I understand now better your argument. If we specify in the interface @implement IsFriendly by be_empathic_with(..., object::Base.Bottom) the interface would be met, if there was a function be_empathic_with(..., x::Whatever) for any type , because Bottom <: Whatever. So I agree, if we had this improved hasmethod, Bottom would be a potential candidate for a default value.

tk3369 commented 4 years ago

Right. Just want to note that the hasmethod function already handles variance properly. For example:

julia> boo(::Any) = 1
boo (generic function with 1 method)

julia> hasmethod(boo, (Any,))
true

julia> hasmethod(boo, (Int,))
true

In this case, the specification of my interface may be to accept an Int argument. But since I have implemented the function with a broader type, Any, the hasmethod figured that a call with an Int argument can be dispatched to my function.

KlausC commented 4 years ago

Ok, good. I must have overlooked something. Actually I tested with Base.Bottom:

julia> Base.Bottom <: Int
true
julia> struct Duck end
julia> Base.Bottom <: Int
true
julia> fly(::Duck, ::Int) = 1
fly (generic function with 1 method)
julia> hasmethod(fly, Tuple{Duck,Base.Bottom})
false
!!!

julia> fly(::Duck, ::Real) = 2
fly (generic function with 2 methods)
julia> hasmethod(fly, Tuple{Duck,Base.Bottom})
true

It looks like hasmethod does not work as expected, if the argument type is concrete.

KlausC commented 4 years ago

Just found this issue related to the Base.Bottom <: Int topic: https://github.com/JuliaLang/julia/issues/30808