Inherit from a concrete type

function func_a(arg::Vector{<: Bool})
  println(arg)
end

function func_b(arg::Vector{Bool})
  println(arg)
end

v = [true, false, false, true]
func_a(v)
func_b(v)

Does it make sense to use the <: operator on a concrete type like Bool? What is the difference between both functions if any? In this case, Bool is a concrete type so nothing can ever be derived from it.

It is impossible for a concrete type to subtype a different concrete type. Technically subtyping isn’t inheritance because structs are not classes. Vector{<:Bool} is short for Vector{T} where T<:Bool; that’s just a type parameter constraint, and the only practical concrete T is indeed Bool. Union{}<:Bool, but Union{} is uninstantiable and abstract.

As shown, an abstract type can actually subtype a concrete type. The practical usage is type selectors, e.g. Type{Bool} <: DataType. Many types are instances of DataType, including itself. However, it can be useful to dispatch more specifically on the abstract Type{Bool} than on the concrete DataType, for example function foo(::Type{Bool})... would only work on calls foo(Bool). Besides this, you can pretty much consider the subtyping hierarchy as a tree with abstract branch nodes and concrete leaf nodes.

5 Likes

Thanks for your answer, but I fail to see some of it relates to my questions.

function func_a(arg::Vector{<: Bool})
  println(arg)
end

function func_b(arg::Vector{Bool})
  println(arg)
end

function func_c(arg::Vector{T}) where T <: Bool
  println(arg)
end

v = [true, false, false, true]

@code_warntype func_a(v)

@code_warntype func_b(v)

@code_warntype func_c(v)

Output:

MethodInstance for func_a(::Vector{Bool})
  from func_a(arg::Vector{<:Bool}) @ Main ...
Arguments
  #self#::Core.Const(func_a)
  arg::Vector{Bool}
Body::Nothing
1 ─ %1 = Main.println(arg)::Core.Const(nothing)
└──      return %1

MethodInstance for func_b(::Vector{Bool})
  from func_b(arg::Vector{Bool}) @ Main ...
Arguments
  #self#::Core.Const(func_b)
  arg::Vector{Bool}
Body::Nothing
1 ─ %1 = Main.println(arg)::Core.Const(nothing)
└──      return %1

MethodInstance for func_c(::Vector{Bool})
  from func_c(arg::Vector{T}) where T<:Bool @ Main ...
Static Parameters
  T = Bool
Arguments
  #self#::Core.Const(func_c)
  arg::Vector{Bool}
Body::Nothing
1 ─ %1 = Main.println(arg)::Core.Const(nothing)
└──      return %1


julia> 

The output related to func_a and func_b is equal. To me that suggests that the use of <: does not make func_a different from func_b. So why is using <: in this situation allowed?

The new function func_c allows for the use of T in the function body, but the arg value is Vector{Bool} for all three. To me this suggests that all three functions can take the exact same argument object and nothing else. Correct?

Correct. Although Julia considers Vector{Bool} to be more specific than Vector{<: Bool}. Also see that Julia regards A, C, and D below as having identical signatures. However, only B will be called.

julia> foo(arg::Vector{<: Bool}) = :A
foo (generic function with 1 method)

julia> foo(arg::Vector{Bool}) = :B
foo (generic function with 2 methods)

julia> foo(arg::Vector{T} where T <: Bool) = :C
foo (generic function with 2 methods)

julia> foo(arg::Vector{T}) where T <: Bool = :D
foo (generic function with 2 methods)

julia> methods(foo)
# 2 methods for generic function "foo" from Main:
 [1] foo(arg::Vector{Bool})
     @ REPL[11]:1
 [2] foo(arg::Vector{T}) where T<:Bool
     @ REPL[13]:1

julia> foo([true])
:B
2 Likes

Only the first paragraph directly addresses your question. The second explains how abstract types can subtype concrete types because it goes against the common misconception that concrete types are leaf nodes in the type hierarchy. It is related to the first paragraph because the distinction between Vector{Bool} and Vector{<:Bool} is that the latter includes abstract parameters like Vector{Union{}}.

They are (see mkitti’s comment showing the :B method is different). Type inference in @code_warntype is done for call signatures, not method signatures. All of your reports had the call signatures func_X(::Vector{Bool}) from the argument v isa Vector{Bool} you provided.

Also note that this concerns what the method table considers overwriting. There are practical differences. The :C method’s T is not usable outside the parametric type, but in more complicated parametric types you can use that to equate multiple parameters. The :D method’s T becomes a static parameter for the method body (you can’t reassign it like you could a local variable), and this can be used to force the compiler to specialize on some arguments in the exceptional cases it doesn’t.

Correct. Although Julia considers Vector{Bool} to be more specific than Vector{<: Bool}. Also see that Julia regards A, C, and D below as having identical signatures. However, only B will be called.

Brilliant. So casually typing <: in a signature can lead to problems that are really hard to find if there is a function with the same signature but missing the <:. Whatever is changed in the function with <: is never executed if the twin is implemented too. Benny points out that with the <: also vectors of Union{} can be accepted with <:, but that use case seems rare, and if tried it is likely to crash the program when elements of the non-existing vector are addressed. So casually mixing prototypes with <: and without <: is confusing and it requires understanding of the specific rule.

They are (see mkitti’s comment showing the :B method is different). Type inference in @code_warntype is done for call signatures, not method signatures. All of your reports had the call signatures func_X(::Vector{Bool}) from the argument v isa Vector{Bool} you provided.

Ah, thanks. I think what you mean is that if I use the @code_warntype macro the argument matters. I have done too much C in my lifetime.

You can only create Vector{Union{}} with undef, and from then on the exception will be accessing #undef elements, not specifically having anything to do with Union{}. Eg

julia> a = Vector{Union{}}(undef, 4)
4-element Vector{Union{}}:
 #undef
 #undef
 #undef
 #undef

julia> a[2]
ERROR: UndefRefError: access to undefined reference

which would be the same error for Vector{Any}(undef, ...) or any other abstract type.

1 Like