Elementwise operation on array of tuples

tuple

#1

I can apply round() elementwise to tuples as in the following example

julia> a = (1.234567,7.654321)
(1.234567, 7.654321)

julia> typeof(a)
Tuple{Float64,Float64}

julia> round.(a)
(1.0, 8.0)

However, this does not work with arrays of tuples

julia> b = [a; a]
2-element Array{Tuple{Float64,Float64},1}:
 (1.23457, 7.65432)
 (1.23457, 7.65432)

julia> round.(b)
ERROR: MethodError: no method matching round(::Tuple{Float64,Float64})

Is there a convenient way to apply a function to each element of an array of tuples or do I have to write a loop?


#2

map or comprehensions maybe?

julia> map(x->round.(x), b)                                                                                                                                              
2-element Array{Tuple{Float64,Float64},1}:                                                                                                                               
 (1.0, 8.0)                                                                                                                                                              
 (1.0, 8.0)                                                                                                                                                              

julia> [round.(x) for x in b]                                                                                                                                            
2-element Array{Tuple{Float64,Float64},1}:                                                                                                                               
 (1.0, 8.0)                                                                                                                                                              
 (1.0, 8.0)                                                                                                                                                              

#3

A variant that still uses broadcast would be:

julia> (x->round.(x)).(b)
2-element Array{Tuple{Float64,Float64},1}:
 (1.0, 8.0)
 (1.0, 8.0)

I thought round.(identity.(b)) would work, but it seems the identity function doesn’t broadcast?!

julia> identity(b)
2-element Array{Tuple{Float64,Float64},1}:
 (1.23457, 7.65432)
 (1.23457, 7.65432)

julia> identity.(b)
2-element Array{Tuple{Float64,Float64},1}:
 (1.23457, 7.65432)
 (1.23457, 7.65432)

julia> round.(identity.(b))
ERROR: MethodError: no method matching round(::Tuple{Float64,Float64})
Closest candidates are:
  round(::Type{BigInt}, ::BigFloat) at mpfr.jl:221
  round(::Float64) at float.jl:352
  round(::Float32) at float.jl:353
  ...

Is that a bug or expected behaviour?


#4

That’s just broadcast(x->round(identity(x)), b) so it’s totally expected.


#5

Thanks for all the answers. Comparing two of the options

function mapround(b)
	map(x -> round.(x), b)
end

function nomapround(b)
	(x -> round.(x)).(b)
end

a = (1.234567,7.654321)
b = [a; a; a; a; a; a; a;]

@time mapround(b)
@time nomapround(b)

0.000002 seconds (6 allocations: 368 bytes)
0.000002 seconds (5 allocations: 352 bytes)

So it seems the second option saves me one allocation. Do I understand it correctly that the second option would also allow for loop fusion in larger dot expressions while map would not?


#6

You are conflating compilation time and runtime. Benchmark using BenchmarkTools:

using Compat                    # workaround for an issue
using BenchmarkTools

mapround(b) = map(x -> round.(x), b)

nomapround(b) = (x -> round.(x)).(b)

a = (1.234567,7.654321)
b = fill(a, 7)

@benchmark mapround($b)
@benchmark nomapround($b)

gives

julia> @benchmark mapround($b)
BenchmarkTools.Trial: 
  memory estimate:  208 bytes
  allocs estimate:  2
  --------------
  minimum time:     63.930 ns (0.00% GC)
  median time:      66.071 ns (0.00% GC)
  mean time:        74.597 ns (7.00% GC)
  maximum time:     1.177 μs (84.75% GC)
  --------------
  samples:          10000
  evals/sample:     982

julia> @benchmark nomapround($b)
BenchmarkTools.Trial: 
  memory estimate:  192 bytes
  allocs estimate:  1
  --------------
  minimum time:     53.977 ns (0.00% GC)
  median time:      56.167 ns (0.00% GC)
  mean time:        62.408 ns (5.13% GC)
  maximum time:     736.473 ns (82.53% GC)
  --------------
  samples:          10000
  evals/sample:     986

@code_warntype tells you that the second one inlines. Frankly, I would not worry about the difference.


#7

Thanks Tamas. That‘s interesting. Just a quick follow up. I ran the @time lines multiple times, so why am I conflating compile and runtime?


#8

Sorry, I was not aware that you ran @time multiple times (it is a good thing to do, but your code did not indicate this). If you did, you were indeed not measuring compilation. But you still get benchmarking noise.


#9

I think using map is more iodiomatic, and I agree the difference in performance shouldn’t matter here.