Same expression returning inconsistent data types brings considerable inconvenience

For example,

function fun(ind)
    a = [1; 2]
    b = [3 4]
    return a[ind]' * reduce(vcat, b[ind])
end
fun([true; true])
fun([true; false])

the result is:

julia> fun([true; true])
11

julia> fun([true; false])
1×1 adjoint(::Vector{Int64}) with eltype Int64:
 3

Why do functions in Julia have inconsistent attitudes towards arrays? When ind = [true; false], a[ind] is an 1-element Vector{Int64} while reduce(vcat, b[ind]) is just a scalar number.

This means a slight variation on the input parameter of a function that even doesn’t change the format or data type may result in quite different returns. (But I hope function fun can always return a scalar.)

This feature brings considerable inconvenience to coding, one of which is what I have mentioned in this post:

To distinguish between a scalar and an 1-element vector may be logically rigorous, but it seems to deviate from our convention. In particular, Julia, as a programming language that brings code closer to mathematical language (e.g., it supports LaTex characters), just happens to be less “mathematical” here, so why? Is this an advanced design with potential advantages (if so, what are the advantages?), or is it a primitive nature as a programming language?

In the end, I would like to ask everyone for advice on how to deal with such problems in linear algebra programming without making the code overly cumbersome?

Thanks!

I’m confused about why the reduce is necessary:

julia> function fun(ind)
           a = [1; 2]
           b = [3 4]
           return a[ind]' * b[ind]
       end
fun (generic function with 1 method)

julia> fun([true; true])
11

julia> fun([true; false])
3
1 Like

The reduce is not just not needed, it is also to blame:

julia> reduce(vcat, [1, 2])
2-element Vector{Int64}:
 1
 2

julia> reduce(vcat, [1])
1

# Always better to pass a suitable init element
julia> reduce(vcat, [1]; init = similar([1], 0))
1-element Vector{Int64}:
 1

On the other hand, boolean indexing is consistent and always returns a vector. Further, as explained in the other thread u' * v always gives a scalar for two vectors.

Also, did you intend to define b as a 1 \times 2 matrix or is a comma or semicolon missing?

2 Likes

I was just taking that as an example. Is there any proper situation that a reduce is needed?

How about the following example?

"""
Return the result of the following expression:
`c' * r`
where `r` is a random vector with a compatible dimension.
"""
function fun2(c)
    dim = length(c)
    r = rand(dim)
    return c' * r
end
fun2([1, 2])
fun2([1])
fun2(1)

The result is:

julia> fun2([1, 2])
1.97973421660914

julia> fun2([1])
0.7936651331121256

julia> fun2(1)
1-element Vector{Float64}:
 0.3585017443018734

What I want to say is that for the expression c^\top r, we mathematically allow c to degenerate into a scalar and the inner product degenerates into a product of two scalars thereby. So I can’t expect the users to be careful to write scalars as single-element vectors when entering function arguments (which is a bit unconventional). Then, who is responsible when the program goes wrong?

Is there a convenient method in Julia that can convert a variable into a vector when it is a scalar? Or I have to use dot(a, b) instead of the natural writing a' * b when calculating the inner product of two vectors?

We have mutiple dispatch in Julia. Please use it.

julia> function fun2(c)
           dim = length(c)
           r = rand(dim)
           return c' * r
       end
fun2 (generic function with 1 method)

julia> fun2(c::Number) = fun2([c])
fun2 (generic function with 2 methods)

julia> fun2(1)
0.08685592968950084
1 Like

Assuming your method signatures aren’t too complex, I’d just do something like:

function fun3(c::AbstractVector)
  # do stuff
  ...
end
fun3(c::Number) = fun3([c])
2 Likes

Thanks! That is a good idea! :handshake:

However, what if the function has lots of similar arguments? Do I have to write methods for all the arguments one by one? For example,

function f(Params...)
    N = length(Params)
    S = 0
    for i = 1:N
        dim = length(Params[i])
        r = rand(dim)
        S += Params[i]' * r 
    end
    return S
end

Can I use some logic when defining a method? Something like:

function f(x::(Int64 ∪ Float64))
    return typeof(x)
end

The root of the problem here is that you’re using length to determine the size of the output of rand. This is inconsistent with what you’re trying to accomplish. Use size instead:

julia> function fun3(c)
           s = size(c)
           r = rand(s...)
           return c' * r
       end
fun3 (generic function with 1 method)

julia> fun3([1, 1])
1.0818115280587444

julia> fun3([1])
0.604859544315378

julia> fun3(1)
0.663513060981263
3 Likes

Thank you very much! I was just about to point out the problem with length and size, although your reply has given me a lot of inspiration.

julia> length([1, 2])
2

julia> length([1])
1

julia> length(1)
1

julia> size([1, 2])
(2,)

julia> size([1])
(1,)

julia> size(1)
()

julia> struct NewDT end

julia> length([NewDT(), NewDT()])
2

julia> length([NewDT()])
1

julia> length(NewDT())
ERROR: MethodError: no method matching length(::NewDT)

julia> size([NewDT(), NewDT()])
(2,)

julia> size([NewDT()])
(1,)

julia> size(NewDT())
ERROR: MethodError: no method matching size(::NewDT)

Why can’t length and size work well with user-defined data type?

julia> typeof(1)
Int64

julia> typeof(NewDT())
NewDT

How should I get the length or size of a Vector{NewDT} or scalar NewDT?

julia> struct NewDT end

julia> Base.length(::NewDT) = 1

julia> Base.size(::NewDT) = ()

julia> length(NewDT())
1

julia> size(NewDT())
()

julia> size([NewDT()])
(1,)

julia> length([NewDT()])
1
1 Like
julia> function f(x::Union{Float64,Int64})
           return typeof(x)
       end
f (generic function with 1 method)

julia> f(5)
Int64

julia> f(5.0)
Float64

julia> f(3.f0)
ERROR: MethodError: no method matching f(::Float32)
The function `f` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  f(::Union{Float64, Int64})
   @ Main REPL[18]:1
2 Likes

Sure, if you’re passing a collection/iterator of collections that you want to consolidate. I don’t know of a use when you’re only interested in linear algebra though, and I wouldn’t think of using it in that context either. Julia is not exclusively built for linear algebra.

1 Like

If there is a function with a matrix A as its input argument, is it my duty to consider the degenerate types of A (e.g., a vector or even a scalar number) or the user’s duty to input the argument strictly in matrix type when invoking the function? Which is more Julian?

The type system distinguishes between matrices, vectors and scalars. Example:

julia> s = 7
7

julia> v = [7, 8]
2-element Vector{Int64}:
 7
 8

julia> m = [7 8]
1×2 Matrix{Int64}:
 7  8

julia> s isa AbstractMatrix
false

julia> v isa AbstractMatrix
false

julia> m isa AbstractMatrix
true

The method dispatch relies on the type system (multiple dispatch). Start using that while writing code.

Yes, I’m aware of this. Thanks.

My argument is (hopefully not the case) that this setup not only leads to some inconsistencies within the Julia language ecosystem (e.g., see this post) but also brings certain inconveniences to programming, especially in the field of mathematics.

I’m trying to use multiple dispatch, but I’m running into some trouble. For example, I have a function that takes multiple input parameters and is implemented as a matrix operation for all the input parameters. However, when calling the function, some or all of the input parameters may degrade to vectors or scalars, which also make sense mathematically. Do I need to separately write degraded form methods for each parameter?

That’s not a thing in Julia. Can you give a specific example.