Confusion about type parameterization and method applicability

I define the type

@kwdef mutable struct Tablet{T}
    id = nothing
    a::T
    # ...
end

My program mostly operates on Vectors of these. One thing I need to do is jusxtopose such vectors to make longer vectors. I overload Base.+ for that:

function (Base.:+)(a::Tablet{Any}, b::Tablet{Any})::Vector{Tablet{Any}}
    [a, b]
end

function (Base.:+)(a::Tablet{Any}, v::Vector{Tablet{Any}})::Vector{Tablet{Any}}
    [a, v...]
end

function (Base.:+)(v::Vector{Tablet{Any}}, b::Tablet{Any})::Vector{Tablet{Any}}
    [v..., b]
end

function (Base.:+)(v1::Vector{Tablet{Any}}, 
                   v2::Vector{Tablet{Any}})::Vector{Tablet{Any}}
    [v1..., v2...]
end

I also overload Base,:* to repeat individual Tablets and Vectors of Tablets:

function (Base.:*)(repeat::Int, t::Tablet{Any})::Vector{Tablet{Any}}
    result = Vector{Tablet{Any}}()
    for i in 1:repeat
	push!(result, copy(t))
    end
    result
end

function (Base.:*)(repeat::Int, v::Vector{Tablet{Any}})::Vector{Tablet{Any}}
    result = Vector{Tablet{Any}}()
    for i in 1:repeat
	append!(result, copy.(v))
    end
    result
end

When I try to combine tablets though, I get errors:

let
	border_color =RGB(0.5, 0.5, 0.5)
	border1 = Tablet(
		a=border_color,
		b=border_color,
		c=border_color,
		d=border_color,
		threading=BackToFront())
	border1 + border1
end
MethodError: no method matching +(::Main.var"workspace#3".Tablet{ColorTypes.RGB{Float64}}, ::Main.var"workspace#3".Tablet{ColorTypes.RGB{Float64}})

Closest candidates are:

+(::Any, ::Any, !Matched::Any, !Matched::Any...) at operators.jl:591

top-level scope@Local: 9[inlined]

Why are my methods on Tablet not candidates?

Thanks.

I guess is one case of type variance: Vector{Int} <: Vector{Real} is false??? · JuliaNotes.jl

1 Like

Here is a start to get this working:

Base.@kwdef mutable struct Tablet{T}
    id::Union{Nothing,Int64} = nothing
    a::T
    # ...
end

function (Base.:+)(a::Tablet{T}, b::Tablet{T}) where T
    [a; b]
end

function (Base.:+)(a::Tablet{T}, v::Vector{Tablet{T}}) where T
    [a; v]
end

function (Base.:+)(v::Vector{Tablet{T}}, b::Tablet{T}) where T
    [v; b]
end

tablet_1 = Tablet{String}( a = "hello")

tablet_2 = tablet_1 + tablet_1

tablet_3 = tablet_2 + tablet_1

A few notes: [a,b] will create a vector where a is the first element and b the second, while [a;b] will concatenate along the first dimension. Annotating return types is usually not necessary. The where T annotation is telling julia that T can be any type, but that it should all be the same type (I couldn’t give you a more technical explanation, someone more savvy can step in). For performance reasons it’s a good idea to make sure your fields are all type annotated.

2 Likes

To make it work for different types it can be written as:

julia> function (Base.:+)(a::Tablet{<:Any}, b::Tablet{<:Any})
           [a, b]
       end

julia> a=Tablet(1,1)
Tablet{Int64}(1, 1)

julia> b=Tablet(1,"two")
Tablet{String}(1, "two")

julia> a+b
2-element Vector{Tablet}:
 Tablet{Int64}(1, 1)
 Tablet{String}(1, "two")

I changed the Tablet arithmetic methods to use ; rather than , as suggested by @JonasWickman, and to specialize on Tablet{<:Any} rather than Tablet{Any} as suggested by @oheil:
I also specialize on Vector{<:Tablet{<:Any}} instead of Vector{<:Tablet{Any}} to avoid what I think is a contravariance issue.

function (Base.:+)(a::Tablet{<:Any}, b::Tablet{<:Any})
    [a; b]
end

function (Base.:+)(a::Tablet{<:Any}, v::Vector{<:Tablet{<:Any}})
    [a; v...]
end

function (Base.:+)(v::Vector{<:Tablet{<:Any}}, b::Tablet{<:Any})
    [v...; b]
end

function (Base.:+)(v1::Vector{<:Tablet{<:Any}}, v2::Vector{<:Tablet{<:Any}})
    [v1...; v2...]
end

function (Base.:*)(repeat::Int, t::Tablet{<:Any})
    result = Vector{Tablet{<:Any}}()
    for i in 1:repeat
	push!(result, copy(t))
    end
    result
end

function (Base.:*)(repeat::Int, v::Vector{<:Tablet{<:Any}})
    result = Vector{Tablet{<:Any}}()
    for i in 1:repeat
	append!(result, copy.(v))
    end
    result
end

I also bneeded to add a method for + with more than two arguments:

function (Base.:+)(v1::Vector{<:Tablet{<:Any}}, vs::Vector{<:Tablet{<:Any}}...)
    result = v1
    for v2 in vs
	result += v2
    end
    result
end

I guess there isn’t a more general method for this in case someone needs right-associative + for their types fopr some reason (an APL interpreter?).

Thanks for your help.

If anyone is curious what this is about, the code is in

For the very patient who want to see the live notebook in Binder, the link is

1 Like

I think if you’re going for this level of generality you could simply define

function (Base.:+)(a::Tablet,b::Tablet)
    [a;b]
end

function (Base.:+)(v1::Vector{Tablet},v2::Vector{Tablet})
    [v1;v2]
end

Vectors that contain a multitude of types may not be as performant as single-typed vectors though. Depending on your use case this may or may not be important.

As far as I know, Tablet == Tablet{<:Any}.

You mean something like this?

julia> function sum(args::Vector{T}...) where T
           result = fill(zero(T), length(args[1]))
           for arg in args
               result .+= arg
           end
           return result
       end
sum (generic function with 1 method)

julia> sum([1,2],[1,2],[1,2])
2-element Vector{Int64}:
 3
 6

No, I mean a + b + c + ....

That parses to a single call Expr expecting an n-ary method for +.

Something like this?

function Base.(:+)(a::Tablet{<: Any}, b::Tablet{<:Any}, c::Table{<:Any}...)
    [a; b; c...]
end
1 Like

Yup. That’d work.