Elementwise operation on array of tuples

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?

1 Like

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)                                                                                                                                                              
5 Likes

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?

1 Like

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

1 Like

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?

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.

2 Likes

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?

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.

2 Likes

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