When to use brodcasting with . vs map

If I want to apply a function across say a Vector in julia, there are two ways I could do it:

map(f, seq)

f.(seq)

Is any way “better” than the other? In fact, is there any difference in the implementation between using map and broadcasting with . ?

1 Like

On a user level, I prefer to use map when f is somehow complicated. It’s usually either when you need to write it as an anonymous function, or when you can’t use syntax sugar and have to call methods explicitly, or when function is so complicated that you want to use do syntax.

Example 1:

f(x, y) = x + y
v = [1, 2, 3]

map(x -> f(x, 1), v)
# vs
(x -> f(x, 1)).(v)

Example 2

v = [(1, 1), (2, 2), (3, 3)]

map(x -> x[2], v)
# vs
getindex.(v, 2)

Example 3:

v = [(0, 1), (1, 2), (2, 3)]

map(v) do x
   println(v[1])
   v[2]*v[2]
end

In all other cases, broadcasting is usually easier to read and use.

3 Likes

map (or a comprehension) is probably faster if seq is a generic iterable collection rather than an array, since broadcasting first calls collect to convert iterables into arrays. For example:

julia> @btime sqrt.(i^2 for i in 1:100);
  325.522 ns (2 allocations: 1.75 KiB)

julia> @btime map(sqrt, i^2 for i in 1:100);
  235.057 ns (1 allocation: 896 bytes)

Notice that the memory allocation is doubled in the broadcast case, because of the extra array allocated by collect.

18 Likes

In example-1, wouldn’t broadcasting look simpler this way:

julia> f.(v,1)
3-element Array{Int64,1}:
 2
 3
 4
3 Likes

Yeah, you are right, not the best illustration. What I was trying to say, there can be situations, when you have to write some sort of anonymous function. Maybe better example is hand-written implementation of the sign function

v = [-10, 0, 10]
map(x -> x > 0 ? 1 : x < 0 ? -1 : 0, v)
4 Likes

So broadcasting will . actually convert seq to an array, then eventually create and return something of the type that seq originally was, and then map directly creates a collection of the same type as the original seq?

I see what you mean here. I probably wouldn’t want to use that anonymous function by broadcasting either…

1 Like

If seq was already an array, no conversion is required. And broadcasting always produces an array, regardless of the type of seq.

If there are multiple arguments, then the behavior of broadcasting and map are quite different:

Broadcasting does… broadcasting, and map does not:

julia> [1] .+ [1, 2, 3]
3-element Array{Int64,1}:
 2
 3
 4

julia> map(+, [1], [1, 2, 3])
1-element Array{Int64,1}:
 2

julia> [1, 1] .+ [1, 2, 3]
ERROR: DimensionMismatch("arrays could not be broadcast to a common size; got a dimension with lengths 2 and 3")

julia> map(+, [1, 1], [1, 2, 3])
2-element Array{Int64,1}:
 2
 3

Also, broadcasting has some “symbolic” definitions for some types:

julia> (1:5) .+ (1:5)
2:2:10

julia> map(+, 1:5, 1:5)
5-element Array{Int64,1}:
  2
  4
  6
  8
 10

Unfortunately I don’t think there is yet a “standard” multi-argument “map” that is requires the arguments to have the same indices. (I asked here.)

3 Likes

Interesting, I didn’t know that. It’d be nice if generators participated in broadcast more naturally, is there an open issue about this?

1 Like

Thanks for pointing this out. I would have expected that map should expect matching dimensions. I think having a “strict map” can also be a good thing to avoid unexpected behavior if you accidentally pass it parameters of different sizes, especially for users that are coming from / used to Python’s map behavior.

Additionally map is preferable for mixed-type tuples. Map will be type-stable, while broadcast wont be.

3 Likes