By: Steven Whitaker
Re-posted from: https://glcs.hashnode.dev/broadcasting
Julia is a relatively new,free, and open-source programming language.It has a syntaxsimilar to that of other popular programming languagessuch as MATLAB and Python,but it boasts being able to achieve C-like speeds.
Unlike other languagesthat focus on technical computing,Julia does not require usersto vectorize their code(i.e., to have one version of a functionthat operates on scalar valuesand another versionthat operates on arrays).Instead,Julia provides a built-in mechanismfor vectorizing functions:broadcasting.
Broadcasting is useful in Juliafor several reasons,including:
- It allows functionsthat operate on scalar values(e.g.,
cos()
)to operate elementwiseon an array of values,eliminating the needfor specialized, vectorized versionsof those functions. - It allows for more efficient memory allocationin certain situations.For example,suppose we have a function,
func
,and we want to computefunc(1, 2)
andfunc(1, 3)
.Instead of broadcastingon[1, 1]
and[2, 3]
,we can broadcaston1
and[2, 3]
,avoiding the memory allocationfor[1, 1]
.
On top of that,Julia provides a very convenient syntaxfor broadcasting,making it so anyonecan easily use broadcasting in their code.
In this post,we will learn what broadcasting is,and we will see several examplesof how to effectively use broadcasting.
This post assumes you already have Julia installed.If you haven’t yet,check out our earlierpost on how to install Julia.
What Is Broadcasting?
Broadcasting essentially is a methodfor calling functions elementwisewhile virtually copying inputsso that all inputs have the same size.(For example,if two inputs to a broadcasted function f
are 1
and [1, 2, 3]
,the first input is treatedas if it is [1, 1, 1]
but without actually allocating memoryfor an array.Then the function is appliedto each pair of inputs:f(1, 1)
, f(1, 2)
, and f(1, 3)
.)
If that definition doesn’t make sense right now,don’t worry,the examples below will help illustrate.
The Dot Syntax
The first thing to know about broadcastingis that it is very convenient to use.
All you need to do is add dots.
For example,if you want to take the square rootof a collection of values,just add a dot (.
):
julia> sqrt.([1, 4, 9]) # Notice the dot after `sqrt`3-element Vector{Float64}: 1.0 2.0 3.0
Vectorizing Operators and Functions
As stated earlier,Julia doesn’t requirevectorized versions of functions.In fact,many functions don’t even have methodsthat take array inputs.Take sqrt
for example:
julia> sqrt([1, 4, 9]) # No dot after `sqrt`ERROR: MethodError: no method matching sqrt(::Vector{Int64})
So, even though sqrt
doesn’t have a vectorized versionexplicitly defined,the dot syntax still allowssqrt
to be applied elementwise.The same applies to other functions and operators:
julia> [1, 2, 3] ^ 3 # No dotERROR: MethodError: no method matching ^(::Vector{Int64}, ::Int64)julia> [1, 2, 3] .^ 3 # With dot3-element Vector{Int64}: 1 8 27julia> uppercase(["hello", "world"]) # No dotERROR: MethodError: no method matching uppercase(::Vector{String})julia> uppercase.(["hello", "world"]) # With dot2-element Vector{String}: "HELLO" "WORLD"
Vectorization even workswith user-defined functions:
julia> myfunc(x) = x * 2myfunc (generic function with 1 method)julia> myfunc.([1, 2])2-element Vector{Int64}: 2 4
Note that some functionsdo have methodsthat operate on arrays,so be careful when decidingwhether a function should apply elementwise.Take cos
as an example:
julia> A = [0 ; /2 /6];julia> cos(A) # Matrix cosine, *not* elementwise cosine2x2 Matrix{Float64}: -0.572989 -0.285823 -0.142912 -0.620626julia> cos.(A) # Add a dot for computing the cosine elementwise2x2 Matrix{Float64}: 1.0 -1.0 6.12323e-17 0.866025
Broadcasting with Multiple Inputs
Broadcasting gets more interestingwhen multiple inputs are involved.Let’s use addition (+
) as an example.
We can add a scalar to each element of an array:
julia> [1, 2, 3] .+ 103-element Vector{Int64}: 11 12 13julia> 10 .+ [1, 2, 3]3-element Vector{Int64}: 11 12 13
We can also sum two arrays elementwise:
julia> [1, 2, 3] .+ [10, 20, 30]3-element Vector{Int64}: 11 22 33
Broadcasting even works with arraysof different sizes.The only requirement is that non-singleton dimensionsmust match across inputs.
julia> [1 2 3; 4 5 6] .+ [10, 20] # Sizes: (2, 3) and (2,)2x3 Matrix{Int64}: 11 12 13 24 25 26julia> [1 2 3; 4 5 6] .+ [10 20] # Sizes: (2, 3) and (1, 2)ERROR: DimensionMismatch: arrays could not be broadcast to a common size; got a dimension with lengths 3 and 2julia> [1 2 3; 4 5 6] .+ [10 20 30] # Sizes: (2, 3) and (1, 3)2x3 Matrix{Int64}: 11 22 33 14 25 36
In the first example([1 2 3; 4 5 6] .+ [10, 20]
),the column vector [10, 20]
was added to each columnof the matrix,while in the second working example([1 2 3; 4 5 6] .+ [10 20 30]
),the row vector [10 20 30]
was added to each rowof the matrix.
Treating Inputs as Scalars
Sometimes,it is usefulto broadcast overonly a subset of the inputs.For example,suppose we have a functionthat scales an input matrix:
julia> myfunc2(X, a) = X * amyfunc2 (generic function with 1 method)
Suppose we want to scale a given matrixby several different scale factors.The result should be an array of matrices,one matrix for each scale factor applied.We might try to use broadcasting:
julia> X = [1 2; 3 4]; a = [10, 20];julia> myfunc2.(X, a)2x2 Matrix{Int64}: 10 20 60 80
But the result is just one matrix!We have one matrix becausewe broadcasted over a
and X
,not just a
.In this case,we need to wrap X
in a single-element Tuple
:
julia> myfunc2.((X,), a)2-element Vector{Matrix{Int64}}: [10 20; 30 40] [20 40; 60 80]
Now we have the result we want:an array where the first entryis X
scaled by a[1]
and the second entryis X
scaled by a[2]
.
So,whenever you need to treat an inputas a scalarfor broadcasting purposes,just wrap it in a Tuple
.
Broadcasting with Dictionaries and Strings
Dictionaries and stringsmay act differently than expectedin broadcasting,so let’s clarify some things here.
First,attempting to broadcast over a dictionarywill throw an error:
julia> d = Dict("key1" => "hello", "key2" => "world")Dict{String, String} with 2 entries: "key2" => "world" "key1" => "hello"julia> println.(d)ERROR: ArgumentError: broadcasting over dictionaries and `NamedTuple`s is reserved
There are different solutionsdepending on the context.For example:
- Treat the dictionary as a scalar:
julia> println.((d,)); # Note that `d` is wrapped in a `Tuple`Dict("key2" => "world", "key1" => "hello")
- Broadcast over the values explicitly:
julia> println.(values(d));worldhello
Regarding strings,strings are treated as scalars,not as collections of characters.For example:
julia> string.("string", [1, 2])2-element Vector{String}: "string1" "string2"
(The above would have erroredif strings were not treated as scalars,because length("string")
is 6
,whereas length([1, 2])
is 2
.)
To broadcast over the charactersin a string,use collect
:
julia> string.(collect("string"), 1:6)6-element Vector{String}: "s1" "t2" "r3" "i4" "n5" "g6"
Summary
In this post,we learned what broadcasting is,and we saw several examplesof how to effectively use broadcastingto apply functions elementwise.
Have any further questions about broadcasting?Feel free to ask themin the comments below!
Does broadcasting make sense now?Move on to thenext post to learn about Julia’s type system!Or,feel free to take a lookat our other Julia tutorial posts!
Additional Links
- Julia documentation about broadcasting:
- Installing Julia and VS Code – A Comprehensive Guide
- How-to guide for installing Julia and VS Code.
- Understanding the Julia Type System
- Introduction to how Julia handles types.