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.
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
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With