Difference in types being inferred in Julia

Viewed 228

I am trying to learn Julia coming from Python and I came across an interesting tidbit of code on exercism.io. The user did an elegant trick of creating tuples containing functions because they are first-class objects in Julia. Building off that I wanted to try something out.

Suppose I have a list:

my_list = zip(0:3, ["wink", "double blink", "close your eyes", "jump"]) |> collect

And I want to create a list composed of 2-element tuples where the second element is a function:

codes = [(i, x -> push!(x,j)) for (i,j) in my_list]
append!(codes, (4, reverse!))

the code fails to run. Examining the signatures in the REPL I realized the first line above generates a list with signature: 4-element Array{Tuple{Int64,var"#68#70"{String}},1}:

whereas if I did the procedure by hand as in the linked code:

    codes = 
    [ (0, i -> push!(i, "wink"))
    , (1, i -> push!(i, "double blink"))
    , (2, i -> push!(i, "close your eyes"))
    , (3, i -> push!(i, "jump"))
    , (4, reverse!)]

I get the correct type: 5-element Array{Tuple{Int64,Function},1}. I am having trouble understanding the difference and why what I am trying to do is not valid code.

2 Answers

First note that you should use push! not append! to add one element at the end of the vector (append! appends elements of the collection to another collection). Now I will concentrate on the main issue assuming you would have used push! in your code.

All elements of code have the same type:

julia> typeof.(codes)
4-element Array{DataType,1}:
 Tuple{Int64,var"#4#6"{String}}
 Tuple{Int64,var"#4#6"{String}}
 Tuple{Int64,var"#4#6"{String}}
 Tuple{Int64,var"#4#6"{String}}

julia> unique(typeof.(codes))
1-element Array{DataType,1}:
 Tuple{Int64,var"#4#6"{String}}

Even more - this type is concrete:

julia> isconcretetype.(typeof.(codes))
4-element BitArray{1}:
 1
 1
 1
 1

(which means that things are going to be type stable and fast, which is good)

In such cases a comprehension sets this type as eltype of the resulting vector.

The problem is that the (4, reverse!) tuple has a different type:

julia> typeof((4, reverse!))
Tuple{Int64,typeof(reverse!)}

so you cannot add it to codes vector, i.e.:

julia> push!(codes, (4, reverse!))
ERROR: MethodError: Cannot `convert` an object of type typeof(reverse!) to an object of type var"#4#6"{String}

Now how to solve it? Set an appropriate eltype for codes vector when creating it like this:

julia> codes = Tuple{Int, Function}[(i, x -> push!(x,j)) for (i,j) in my_list]
4-element Array{Tuple{Int64,Function},1}:
 (0, var"#7#8"{String}("wink"))
 (1, var"#7#8"{String}("double blink"))
 (2, var"#7#8"{String}("close your eyes"))
 (3, var"#7#8"{String}("jump"))

julia> push!(codes, (4, reverse!))
5-element Array{Tuple{Int64,Function},1}:
 (0, var"#7#8"{String}("wink"))
 (1, var"#7#8"{String}("double blink"))
 (2, var"#7#8"{String}("close your eyes"))
 (3, var"#7#8"{String}("jump"))
 (4, reverse!)

and all will work as expected.

Let me give a simpler example of the same problem, so that the issue is more clearly visible:

julia> x = [i for i in 1:3]
3-element Array{Int64,1}:
 1
 2
 3

julia> eltype(x)
Int64

julia> push!(x, 1.5)
ERROR: InexactError: Int64(1.5)
Stacktrace:
 [1] Int64 at ./float.jl:710 [inlined]
 [2] convert at ./number.jl:7 [inlined]
 [3] push!(::Array{Int64,1}, ::Float64) at ./array.jl:913
 [4] top-level scope at REPL[55]:1

julia> x = Float64[i for i in 1:3]
3-element Array{Float64,1}:
 1.0
 2.0
 3.0

julia> push!(x, 1.5)
4-element Array{Float64,1}:
 1.0
 2.0
 3.0
 1.5

and append! would work like this (continuing the last example):

julia> append!(x, [2.5, 3.5])
6-element Array{Float64,1}:
 1.0
 2.0
 3.0
 1.5
 2.5
 3.5

What a fascinating question! First, let's look at the output of the array comprehension:

julia> codes = [(i, x -> push!(x,j)) for (i,j) in my_list]
4-element Array{Tuple{Int64,var"#50#52"{String}},1}:
 (0, var"#50#52"{String}("wink"))
 (1, var"#50#52"{String}("double blink"))
 (2, var"#50#52"{String}("close your eyes"))
 (3, var"#50#52"{String}("jump"))

Intestingly, you can see that all the functions in the vector are called var"#50#52"{String}(SOMETHING). We can get the type of one of these functions:

julia> typeof(codes[1][2])
var"#50#52"{String}

And see that is indeed a subtype of Function:

julia> typeof(codes[1][2]) <: Function
true

In fact, it appears that the four functions are the same type:

julia> all(typeof(f) === typeof(codes[1][2]) for (i, f) in codes)
true

It therefore appears that, for the sake of efficiency, Julia creates a single function type and 4 instances of the function each referring to a different string.

In Julia, every function has its own type. As shown above, this anonymous function has the type var"#50#52"{String}. The array specializes, setting its element type to the most specific type applicable. Therefore, the array's element type is Tuple{Int64,var"#50#52"{String}}, as can also be seen in the first snippet above. This means that the array only can contain that function in particular!

The same happens if you create an array with a normal function:

julia> array = [reverse!]
1-element Array{typeof(reverse!),1}:
 reverse!

julia> push!(array, isodd)
ERROR: MethodError: Cannot `convert` an object of type typeof(isodd) to an object of type typeof(reverse!)

To solve this, you need to instantiate the array so that it can contain any function:

Tuple{Int,Function}[(i, x -> push!(x,j)) for (i,j) in my_list]

And then it works :)

j

Related