Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Difference in types being inferred in Julia

Tags:

julia

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.

like image 396
ITA Avatar asked May 12 '20 10:05

ITA


2 Answers

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

like image 73
Jakob Nissen Avatar answered Sep 21 '22 03:09

Jakob Nissen


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
like image 43
Bogumił Kamiński Avatar answered Sep 21 '22 03:09

Bogumił Kamiński