Skip to content

Commit 0958adf

Browse files
colinleachColin Leachdepial
authored
Vector operations (rescue attempt) (exercism#842)
* vector-operations, another attempt * text cleanup * Update config.json --------- Co-authored-by: Colin Leach <[email protected]> Co-authored-by: depial <[email protected]>
1 parent d1fba68 commit 0958adf

File tree

5 files changed

+430
-0
lines changed

5 files changed

+430
-0
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"authors": [
3+
"colinleach"
4+
],
5+
"contributors": [],
6+
"blurb": "Julia provides many ways to operate on an array, either as a unit or element-wise."
7+
}

concepts/vector-operations/about.md

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
# About
2+
3+
In the [`Vectors`][vectors] Concept, we said that "arrays are at the heart of the Julia language" and a vector is a 1-dimensional array.
4+
5+
Given this, we could reasonably hope that the language provides many versatile and powerful ways to _do things_ with vectors, whatever that means.
6+
7+
A note on terminology: though this document talks a lot about "vectors", much of it also applies to any iterable type: [ranges][ranges], [tuples][tuples], [sets][sets], and various others.
8+
9+
## Functions expecting vector input
10+
11+
Some very simple functions take a vector input and (for 1-D input) return a scalar output.
12+
13+
```julia
14+
v = [2, 3, 4]
15+
length(v) # => 3
16+
sum(v) # => 9
17+
```
18+
19+
When we reach the Concept on multidimensional arrays, it will become clearer that this is _dimension reduction_ rather than necessarily returning a scalar.
20+
If that makes no sense to you, skip worrying about it for now.
21+
22+
There are many more functions of this type.
23+
See the [`Statistics`][statistics] Concept for some examples.
24+
25+
There are also functions that operate on multiple vectors, such as the (very useful) [`zip`][zip].
26+
27+
```julia-repl
28+
julia> z = zip( 1:3, ['a', 'b', 'c'], ["I", "make", "tuples"] )
29+
zip([1, 2, 3], ['a', 'b', 'c'], ["I", "make", "tuples"])
30+
31+
# convert iterator to vector
32+
julia> collect(z)
33+
3-element Vector{Tuple{Int64, Char, String}}:
34+
(1, 'a', "I")
35+
(2, 'b', "make")
36+
(3, 'c', "tuples")
37+
```
38+
39+
`zip()` takes an arbitrary number of vector-like inputs and returns an iterator of tuples.
40+
41+
The inputs are usually all the same length.
42+
If one is shorter, the others are truncated to the shortest length: _maybe_ what you intended, but _more commonly_ a bug in your code.
43+
44+
## Arithmetic
45+
46+
Suppose you have a numerical vector and want to subtract 0.5 from each value.
47+
48+
```julia-repl
49+
julia> v = [1.2, 1.5, 1.7]
50+
3-element Vector{Float64}:
51+
1.2
52+
1.5
53+
1.7
54+
55+
julia> v - 0.5
56+
ERROR: MethodError: no method matching -(::Vector{Float64}, ::Float64)
57+
```
58+
59+
That fails, so what about subtracting another vector?
60+
61+
```julia-repl
62+
julia> v - [0.5, 0.5, 0.5]
63+
3-element Vector{Float64}:
64+
0.7
65+
1.0
66+
1.2
67+
```
68+
69+
Successful, but quite tedious and memory-hungry as the vectors get longer.
70+
71+
Depending on how far you have reached in the syllabus, you can probably think of other approaches:
72+
73+
- Write a loop, though this would be verbose and clunky.
74+
- Use a comprehension: `[x - 0.5 for x in v]` gives the desired result (Python-style).
75+
- Use a higher-order function: `map(x -> x - 0.5, v)` also works (Haskell-style, though common in many languages).
76+
77+
Fortunately, Julia has a "magic" dot to solve this problem very simply: `v .- 0.5` is all you need.
78+
79+
The next section explains why.
80+
81+
## [Broadcasting][broadcasting]
82+
83+
So, `v - 0.5` fails but `v .- 0.5` succeeds, and we need to understand what the dot is doing.
84+
85+
Two things, which combine to give the desired result.
86+
87+
### 1) Element-wise application
88+
89+
Firstly, adding a dot _before_ any infix operator means "apply this operation to each element separately".
90+
91+
Similarly, adding a dot _after_ a function name "vectorizes" it, even if the function was written for scalar inputs.
92+
93+
```julia-repl
94+
julia> sqrt.([1, 4, 9])
95+
3-element Vector{Float64}:
96+
1.0
97+
2.0
98+
3.0
99+
```
100+
101+
As an aside, infix operators are really just syntactic sugar for the underlying function.
102+
103+
This means that, for example, `[1, 5, 10] .% 3` is translated to ` mod.([1, 5, 10], 3)` by the interpreter, and the `mod.` syntax then executes (both versions return `[1, 2, 1]`).
104+
105+
### 2) Singleton expansion
106+
107+
We saw in a previous example that we can subtract vectors of equal length, though please understand that `.-` is a _safer_ operator than `-` by making the element-wise intention clear.
108+
109+
```julia-repl
110+
julia> v .- [0.5, 0.5, 0.5]
111+
3-element Vector{Float64}:
112+
0.7
113+
1.0
114+
1.2
115+
```
116+
117+
What about vectors of unequal length?
118+
119+
```julia-repl
120+
julia> v .- [0.5, 0.5]
121+
ERROR: DimensionMismatch: arrays could not be broadcast to a common size
122+
123+
julia> v .- [0.5,]
124+
3-element Vector{Float64}:
125+
0.7
126+
1.0
127+
1.2
128+
```
129+
130+
In general, unequal lengths are an error, _except_ when one has length 1 (technically, a "singleton" dimension).
131+
132+
Singletons like `[0.5,]` or just `0.5` are automatically expanded to the necessary length by repetition.
133+
This is at the heart of `broadcasting`.
134+
135+
Anyone worrying about memory usage from this "repetition" can relax: it is implemented in a very efficient way that does not actually copy the values in memory.
136+
137+
Programmers familiar with broadcasting in other languages should note that Julia's approach is (mostly) similar to NumPy, but much less tolerant of size mismatches than R.
138+
139+
### Un-dotted operators: a cautionary tale
140+
141+
This subsection is rather math-heavy, so most students are not expected to really understand it.
142+
However, it is a useful warning that may help with debugging when you see unexpected error messages.
143+
144+
```julia-repl
145+
julia> v = [1, 2, 3]
146+
3-element Vector{Int64}:
147+
1
148+
2
149+
3
150+
151+
julia> v * v
152+
ERROR: MethodError: no method matching *(::Vector{Int64}, ::Vector{Int64})
153+
154+
# look, no commas
155+
julia> u = [1 2 3]
156+
1×3 Matrix{Int64}:
157+
1 2 3
158+
159+
julia> u * v
160+
1-element Vector{Int64}:
161+
14
162+
163+
julia> v * u
164+
3×3 Matrix{Int64}:
165+
1 2 3
166+
2 4 6
167+
3 6 9
168+
```
169+
170+
If you happen to have a background in linear algebra then (1) you are not a typical Exercism user _(but very welcome here!)_ and (2) you may recognize that `v` is a column vector, `u` is a row vector, `u * v` is the inner product and `v * u` is the outer product.
171+
_Julia follows the rules of mathematics, in this as in everything_.
172+
173+
**For everyone else:** please just understand why we recommend you should always use dotted operators for element-wise calculations: `v .* v` works exactly as you might expect, to give `[1, 4, 9]`.
174+
175+
## Indexing
176+
177+
Selecting elements of a vector by index number has been discussed in previous Concepts.
178+
179+
```julia
180+
a = collect('A':'Z') # => 26-element Vector{Char}
181+
182+
# index with an integer
183+
a[2] # => 'B'
184+
185+
# index with a range
186+
a[12:2:18] # => ['L', 'N', 'P, 'R']
187+
188+
# index with another vector
189+
a[ [1, 3, 5] ] # => ['A', 'C', 'E']
190+
```
191+
192+
### Logical indexing
193+
194+
It is also possible to select elements that satisfy some logical expression (technically, a "predicate").
195+
This usually requires broadcasting.
196+
197+
```julia-repl
198+
julia> a[a .< 'D']
199+
3-element Vector{Char}:
200+
'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
201+
'B': ASCII/Unicode U+0042 (category Lu: Letter, uppercase)
202+
'C': ASCII/Unicode U+0043 (category Lu: Letter, uppercase)
203+
```
204+
205+
For more complex expression the dots tend to proliferate (but they are small and easy to type).
206+
207+
```julia-repl
208+
julia> a[a .< 'D' .|| a .> 'W']
209+
6-element Vector{Char}:
210+
'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
211+
'B': ASCII/Unicode U+0042 (category Lu: Letter, uppercase)
212+
'C': ASCII/Unicode U+0043 (category Lu: Letter, uppercase)
213+
'X': ASCII/Unicode U+0058 (category Lu: Letter, uppercase)
214+
'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
215+
'Z': ASCII/Unicode U+005A (category Lu: Letter, uppercase)
216+
```
217+
218+
A reminder that the "vector" can in fact be any appropriate ordered iterable, such as a range:
219+
220+
```julia-repl
221+
julia> n = 3:10
222+
3:10
223+
224+
julia> n[isodd.(n)]
225+
4-element Vector{Int64}:
226+
3
227+
5
228+
7
229+
9
230+
```
231+
232+
Internally, the predicate is converted to a [`BitVector`][bitarray] which is then used as an index.
233+
234+
```julia-repl
235+
julia> condition = a .< 'D'
236+
26-element BitVector:
237+
1
238+
1
239+
1
240+
0
241+
# display truncated
242+
243+
julia> a[condition]
244+
3-element Vector{Char}:
245+
'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
246+
'B': ASCII/Unicode U+0042 (category Lu: Letter, uppercase)
247+
'C': ASCII/Unicode U+0043 (category Lu: Letter, uppercase)
248+
```
249+
250+
[vectors]: https://exercism.org/tracks/julia/concepts/arrays
251+
[ranges]: https://exercism.org/tracks/julia/concepts/ranges
252+
[sets]: https://exercism.org/tracks/julia/concepts/sets
253+
[tuples]: https://exercism.org/tracks/julia/concepts/tuples
254+
[statistics]: https://exercism.org/tracks/julia/concepts/statistics
255+
[zip]: https://docs.julialang.org/en/v1/base/iterators/#Base.Iterators.zip
256+
[bitarray]: https://docs.julialang.org/en/v1/base/arrays/#Base.BitArray
257+
[broadcasting]: https://docs.julialang.org/en/v1/manual/arrays/#Broadcasting

0 commit comments

Comments
 (0)