Search code examples
iteratorjuliasplat

In Julia how to deal neatly with Zero Dimensions and Iterators


Consider the following scenario:

do_something( v :: Vector{ T } ) where { T <: Integer } = println( "v = $v" )

function d_dep( d :: Integer )

    axis_iters = fill( -1:1, d )

    for ig in Iterators.product( axis_iters ... )
        g = [ ig ... ]
        do_something( g )
    end
    
end

This is a model of what I am doing in another code, and does what I want if I provide a positive integer argument to d_dep:

julia> include( "nd.jl" )
d_dep (generic function with 1 method)

julia> d_dep( 1 )
v = [-1]
v = [0]
v = [1]

julia> d_dep( 2 )
v = [-1, -1]
v = [0, -1]
v = [1, -1]
v = [-1, 0]
v = [0, 0]
v = [1, 0]
v = [-1, 1]
v = [0, 1]
v = [1, 1]

julia>

Unfortunately d=0 means something in this case, and that's where things go pear-shaped:

julia> d_dep(0)
ERROR: MethodError: no method matching do_something(::Vector{Any})

Closest candidates are:
  do_something(::Vector{T}) where T<:Integer
   @ Main ~/julia/test/nd.jl:1

Stacktrace:
 [1] d_dep(d::Int64)
   @ Main ~/julia/test/nd.jl:9
 [2] top-level scope
   @ REPL[4]:1

julia>

The problem as I understand it is that when you apply the splat operator to the array axis_iters it gets expanded to a empty list from which Iterators.Product has nothing to get a type from, hence Vector{Any}. So two questions:

  1. Currently I have a if d == 0 in d_dep. This is ugly. Is there a neater way to deal with the zero d case, or the whole structure in general? If it helps d should be positive semi-definite.

  2. Actually I don't really understand why when d=0 the loop is executed at all. Why is it - is Julia like Fortran66 was alleged to be (but wasn't) in that it was mandated that all loops have at least 1 trip? [ In F66 zero trip loops were actually implementation defined ]


Solution

  • The result of Iterators.product() is a zero-dimensional iterator. In Julia a zero-dimensional iterators (things like Array{Int, 0}) are implemented to behave like a scalar

    And you can see that you can iterate over a scalar:

    julia> for i in 1
               @show i
           end
    i = 1
    

    So it should not be that surprising that you can iterate over a zero-dimensional Array.

    This is basically a language design decision, made (AFAIK) to make some broadcasting cases like [0, 1, 2] .+ 1 work consistently. You can read more here.

    As for making the code work, I see two options:

    Always make typeof(g) == Vector{Int}, for example:

    function d_dep(d::Integer)
        axis_iters = fill(-1:1, d)
    
        for ig in Iterators.product(axis_iters...)
            g = Int[ig...]
            do_something(g)
        end
    end
    

    Or add another do_something method to catch the d==0 case:

    julia> do_something(v::Vector) = println("v = $v")
    
    julia> d_dep(0)
    v = Any[]