Neil
Neil

Reputation: 1036

Creating Arrays from Ranges in Julia without using Collect()

I am a bit puzzled by this behavior in Julia when creating arrays from ranges. I want to know the underlying mechanics of the following.

A = [1:10]

Results in 1-element Array{UnitRange{Int64},1}

which is not what I wanted. Above code creates an Array of UnitRange. Julia documentation recommends using collect() to create arrays from ranges as follows:

A = collect(1:10)

Results in 10-element Array{Int64,1}. Perfect.

However, this code also works if I add a semicolon after the range.

A = [1:10;]

According to Julia documentation, ; is a short hand for vcat() or vertical concatenation. What is the significance of vcat when using it as A = [1:10;]. Not only does it look weird (what is it vcat'ing it with?), it doesn't make sense to me.

I'd love a clear explanation about how ranges interact with vcat.

Upvotes: 15

Views: 16535

Answers (1)

Gnimuc
Gnimuc

Reputation: 8566

ranges are "lazy" vectors that never allocate. It's probably one of the most useful iterators.

julia> AbstractRange <: AbstractVector
true

julia> @allocated [1,2,3,4,5,6,7,8,9,10]
160

julia> @allocated 1:10
0

the range operator : is for creating ranges:

julia> 1:10 |> dump
UnitRange{Int64}
  start: Int64 1
  stop: Int64 10

you've already known how to convert ranges to vectors using collect, but if you could dive a little bit deeper into the code, you would find collect actually calls vcat under the hood:

julia> @less collect(1:10)

collect(r::AbstractRange) = vcat(r) 

and this is how vcat deal with AbstractRange input:

@less vcat(1:10)    

function vcat(rs::AbstractRange{T}...) where T
    n::Int = 0
    for ra in rs
        n += length(ra)
    end
    a = Vector{T}(undef, n)
    i = 1
    for ra in rs, x in ra
        @inbounds a[i] = x
        i += 1
    end
    return a
end

the implementation is very simple, just looping through the input (note rs is a vararg input), and concating input ranges one-by-one into a single vector. obviously, it works even when there is only one input range, which is the case of [1:10;].

there is another way to create vectors from ranges: directly calling the Vector constructor Vector(1:10). but what happens under the hood? simply calling @less Vector(1:10) won't directly jump to the original implementation, and this is where the fancy debugger comes in:

julia> using Debugger

julia> @enter Vector(1:10)
In Type(x) at boot.jl:424
>424  (::Type{Array{T,N} where T})(x::AbstractArray{S,N}) where {S,N} = Array{S,N}(x)

About to run: (Core.apply_type)(Array, Int64, 1)
1|debug> s
In Type(x) at boot.jl:424
>424  (::Type{Array{T,N} where T})(x::AbstractArray{S,N}) where {S,N} = Array{S,N}(x)

About to run: (Array{Int64,1})(1:10)
1|debug> s
[ Info: tracking Base
In Type(r) at range.jl:943
>943  Array{T,1}(r::AbstractRange{T}) where {T} = vcat(r)

About to run: (vcat)(1:10)
1|debug> s
In vcat(rs) at range.jl:930
>930  n::Int = 0
 931  for ra in rs
 932      n += length(ra)
 933  end
 934  a = Vector{T}(undef, n)

About to run: Core.NewvarNode(:(_5))

as you can see, Vector also calls vcat.

I think this example has already given you some ideas about how to interactively find the answer in Julia REPL by yourself with these great handy built-in reflection tools. there are other useful tools like @code_lowered, @code_typed, @macroexpand etc. that could help you to figure out questions like "what does this expression do?", for example,

julia> f() = [1:10;]
f (generic function with 1 method)

julia> @code_lowered f()
CodeInfo(
1 ─ %1 = 1:10
│   %2 = (Base.vcat)(%1)
└──      return %2
)

the "lowered" code tells us that Julia firstly create a range %1 = 1:10 and then call Base.vcat(%1), which is exactly what the documentation said.

X-ref: What is the difference between @code_native, @code_typed and @code_llvm in Julia?

Upvotes: 19

Related Questions