Why does `1 * String[]` return a Vector{Union{}}?

1 * ["a"] # gives error
1 * String[] # returns Vector{Union{}}

Why is the second allowed?

2 Likes

We can ask Julia which method implements it

julia> @which 1 * String[]
*(A::Number, B::AbstractArray)
     @ Base arraymath.jl:21

This makes sense as the idea is c \cdot v for c \in \mathbb k and v a vector would then become a component-wise multiply, useful when you implement your own types.

Hopefully this explains why it is allowed, but another question would be should such behaviour be considered wrong.

2 Likes

I understand that a scalar times a vector is essentially a component-wise multiplication. (I think it calls broadcast?). But my question is, why is the operation * between an Int64 and a Vector{String} allowed when *(::Int64, ::String) is not defined?

You can not do, for example, 1 * "a" because *(::Int64, ::String) is not defined. And this is the error you get when you try 1 * ["a"] as well. That makes sense. So then why allow 1 * String[]?

* between Int64 and Vector{String} is defined. It will try to broadcast the multiplication across the vector. *(Int64, String) does not exists to trying to call it will result in a MethodError. But here is the catch: Since your vector is empty, no such call occurs, so no error is thrown!

You can also see this from the type of the resulting Vector{Union{}}. Union{} means there are no possible types that could fit into this vector which is a consequence of *(Int64, String) not being defined.

Perhaps, we could also see this case as an instance of “everything is true of the elements of the empty set”.

6 Likes

Ok, the type of String[] is getting ignored there. @assert eltype(String[]) == String.

I don’t know if I like empty arrays. I tend to use them as default values. I’ll need to rethink them and make sure to treat them as special cases. It turns out that all elements of empty array are true, even when the array cannot hold Bools:

@assert all(String[])

But all elements of the same, non-Boolean empty arrays are also false:

@assert !any(String[])

I don’t understand why the eltype gets ignored.

Perhaps this topic can help:

4 Likes

Thanks, @Sukera. I had no doubt that people had put thought into the choice of behavior. But things are not any clearer to me. (And maybe that’s okay.) I suppose all programming languages have to deal with empty sets/arrays. There are two things that confuse me.

  1. Why all and any and the like work on non-Boolean arrays at all. Probably for the same reason that 'a' + 1 == 'b' and 1 == true.

  2. Why the need to return an answer when there isn’t one? Maybe any([]) and all([]) should be neither true nor false. I’d rather deal with an error there and implement an if !isempty to deal with the error than have some unexpected value floating around my program. And it’s not even that all and any necessarily return Bool. They return missing when the iterator contains missing values.

They only work in the empty case, because the empty case doesn’t care about the type; there is no logical predicate you can write that will make the any call return true, because it won’t ever be applied. Once you have a non-empty array, you do get an error:

julia> any(["foo"])
ERROR: TypeError: non-boolean (String) used in boolean context
Stacktrace:
 [1] _any
   @ Base ./reduce.jl:1228 [inlined]
 [2] _any
   @ Base ./reducedim.jl:1007 [inlined]
 [3] any(a::Vector{String})
   @ Base ./reducedim.jl:1005
 [4] top-level scope
   @ REPL[1]:1

But there is one - any(f, vec) is asking the question “of all the elements in vec, is there one that fulfills the predicate f?” and for an empty array the answer is false, because there is no example from vec you could provide to make that true. You can think of any/all as a fold, if you will, starting out with initial values of false and true respectively. If there are no elements to fold over, it’s just the initial value being returned.

That’s just a property of missing though, and unrelated to any/all. Your predicate is free to handle missing differently.

1 Like

The simplest mental model here is that any is true if there’s at least one true and all is false if there’s at least one false. Array element types don’t matter; it’s the values that do. In fact:

julia> all([false, "no errors!"])
false

julia> any([true, "no errors!"])
true
9 Likes

any(f, vec) is asking the question “of all the elements in vec, is there one that fulfills the predicate f?”

I like the framing as a mnemonic:

any(f, vec): There is an element in vec that fulfills the predicate f.
all(f, vec): There is no element in vec that fails to fulfill the predicate f.

But again, this leads to the contradiction (all(f, []) && !any(f, [])) == true, as mentioned in the post linked earlier. And now that I think about it, all(f, []) == all(!f, []) == true.

So we’re just kicking the can down the road-- does the element of an empty set, the one that does not exist, fulfill or fail to fulfill a predicate?

I wrote this just to think through the issue a bit, and defaults are really sneaky when dealing with empty sets. I wanted to leave it here as a cautionary tale.

function allisodd(set::Vector)
    for element in set
        if isodd(element) == false  # element fails to fulfill predicate
            return false
        end
    end

    return true
end
function noneisodd(set::Vector)
    for element in set
        if isodd(element) == true # element fulfills predicate
            return false
        end
    end

    return true
end

With this, we get allisodd([]) == noneisodd([]) == true

I’ll live with the mnemonic for now. Thanks for engaging.