У концепції Vectors ми казали, що «масиви є серцем мови Julia», а вектор є одновимірним масивом.
З огляду на це, ми могли б цілком сподіватися, що мова надає багато універсальних і потужних способів робити щось з векторами, хоч би що це означало.
Примітка щодо термінології: хоча цей документ багато говорить про «вектори», значна частина сказаного також стосується будь-якого типу, який можна перебирати: діапазонів, кортежів, множин та різних інших.
Деякі дуже прості функції приймають векторні вхідні дані й (для одновимірних вхідних даних) повертають скалярні вихідні дані.
v = [2, 3, 4]
length(v) # => 3
sum(v) # => 9
Коли ми дійдемо до концепції багатовимірних масивів, стане зрозуміліше, що це зменшення розмірності, а не обовʼязково повернення скаляра. Якщо це поки що не має сенсу, не переймаймося цим зараз.
Є багато інших функцій цього типу.
Деякі приклади дивіться в концепції Statistics.
Є також функції, які працюють із кількома векторами, як-от (дуже корисна) zip.
julia> z = zip( 1:3, ['a', 'b', 'c'], ["I", "make", "tuples"] )
zip([1, 2, 3], ['a', 'b', 'c'], ["I", "make", "tuples"])
# convert iterator to vector
julia> collect(z)
3-element Vector{Tuple{Int64, Char, String}}:
(1, 'a', "I")
(2, 'b', "make")
(3, 'c', "tuples")
zip() приймає довільну кількість вектороподібних вхідних даних і повертає ітератор кортежів.
Зазвичай усі вхідні дані мають однакову довжину. Якщо один із них коротший, інші обрізаються до найкоротшої довжини: можливо, саме цього ми й хотіли, але частіше це помилка в нашому коді.
Припустімо, у нас є числовий вектор і ми хочемо відняти 0.5 від кожного значення.
julia> v = [1.2, 1.5, 1.7]
3-element Vector{Float64}:
1.2
1.5
1.7
julia> v - 0.5
ERROR: MethodError: no method matching -(::Vector{Float64}, ::Float64)
Це не працює, тож що щодо віднімання іншого вектора?
julia> v - [0.5, 0.5, 0.5]
3-element Vector{Float64}:
0.7
1.0
1.2
Успішно, але досить нудно й марнотратно щодо памʼяті, коли вектори стають довшими.
Залежно від того, як далеко ми просунулися в силабусі, ми, ймовірно, можемо придумати інші підходи:
[x - 0.5 for x in v] дає бажаний результат (у стилі Python).map(x -> x - 0.5, v) також працює (у стилі Haskell, хоча це поширено в багатьох мовах).На щастя, у Julia є «магічна» крапка, яка розвʼязує цю проблему дуже просто: v .- 0.5 — це все, що потрібно.
Наступний розділ пояснює чому.
Отже, v - 0.5 не працює, а v .- 0.5 працює, і нам потрібно зрозуміти, що робить крапка.
Дві речі, які разом дають бажаний результат.
По-перше, додавання крапки перед будь-яким інфіксним оператором означає «застосувати цю операцію до кожного елемента окремо».
Так само додавання крапки після назви функції «векторизує» її, навіть якщо функцію написано для скалярних вхідних даних.
julia> sqrt.([1, 4, 9])
3-element Vector{Float64}:
1.0
2.0
3.0
До речі, інфіксні оператори насправді є лише синтаксичним цукром для відповідної функції.
Це означає, що, наприклад, [1, 5, 10] .% 3 інтерпретатор перетворює на mod.([1, 5, 10], 3), а потім виконується синтаксис mod. (обидві версії повертають [1, 2, 1]).
У попередньому прикладі ми бачили, що можемо віднімати вектори однакової довжини, але, будь ласка, звернімо увагу: .- є безпечнішим оператором, ніж -, бо чітко показує поелементний намір.
julia> v .- [0.5, 0.5, 0.5]
3-element Vector{Float64}:
0.7
1.0
1.2
А як щодо векторів різної довжини?
julia> v .- [0.5, 0.5]
ERROR: DimensionMismatch: arrays could not be broadcast to a common size
julia> v .- [0.5,]
3-element Vector{Float64}:
0.7
1.0
1.2
Загалом різна довжина є помилкою, за винятком випадку, коли один має довжину 1 (технічно, «одиничний» вимір).
Одиничні значення, як-от [0.5,] або просто 0.5, автоматично розширюються до потрібної довжини через повторення.
Це лежить в основі broadcasting.
Усі, хто хвилюється через використання памʼяті від цього «повторення», можуть заспокоїтися: воно реалізовано дуже ефективно й насправді не копіює значення в памʼяті.
Програмістам, знайомим із броадкастингом в інших мовах, варто зауважити, що підхід Julia (здебільшого) подібний до NumPy, але набагато менш терпимий до невідповідностей розмірів, ніж R.
Якщо використання памʼяті викликає занепокоєння, то операції на місці є поширеним способом зменшити кількість виділень памʼяті.
Однак операції броадкастингу в наведених вище прикладах створюють новий Vector, а не змінюють початковий.
julia> v = [1, 2, 3]
3-element Vector{Int64}:
1
2
3
julia> v .+ 1
3-element Vector{Int64}:
2
3
4
julia> v
3-element Vector{Int64}:
1
2
3
Щоб змінити v на місці, зміну потрібно застосувати через броадкастинг.
julia> v .= v .+ 1
3-element Vector{Int64}:
2
3
4
julia> v .+= 1 # equivalent operation to above
3-element Vector{Int64}:
3
4
5
julia> v
3-element Vector{Int64}:
3
4
5
Але будьмо обережні! Крапка перед оператором присвоєння .= важлива.
julia> v = [1, 2, 3];
julia> v = v .+ 1
3-element Vector{Int64}:
2
3
4
julia> v
3-element Vector{Int64}:
2
3
4
Здається, це працює так само, але тут v .+ 1 створив новий вектор [2, 3, 4], а потім присвоїв його змінній v, залишивши початковий вектор [1, 2, 3] у памʼяті, щоб його прибрав збирач сміття.
У результаті використовується вдвічі більше памʼяті, ніж у попередньому прикладі, який повторно використовує памʼять, виділену для початкового вектора.
Так само є повʼязані тонкощі, коли ми виконуємо броадкастинг із векторами однакового розміру.
Наприклад, для двох векторів v і w однакового розміру:
v .= w створює copy від w у ділянці памʼяті v. Подальші зміни у v не впливають на w, і навпаки.v = w створює ще один вказівник на ділянку памʼяті w з назвою v. Подальші зміни у v відображаються на w, і навпаки.Цей підрозділ досить насичений математикою, тож більшість учнів не зобовʼязані його до кінця розуміти. Однак це корисне застереження, яке може допомогти під час налагодження, коли ми бачимо несподівані повідомлення про помилки.
julia> v = [1, 2, 3]
3-element Vector{Int64}:
1
2
3
julia> v * v
ERROR: MethodError: no method matching *(::Vector{Int64}, ::Vector{Int64})
# look, no commas
julia> u = [1 2 3]
1×3 Matrix{Int64}:
1 2 3
julia> u * v
1-element Vector{Int64}:
14
julia> v * u
3×3 Matrix{Int64}:
1 2 3
2 4 6
3 6 9
Якщо хтось має досвід у лінійній алгебрі, то (1) він не типовий користувач Exercism (але йому тут дуже раді!) і (2) він може впізнати, що v є вектором-стовпцем, u є вектором-рядком, u * v є внутрішнім добутком, а v * u є зовнішнім добутком.
Julia дотримується правил математики, у цьому як і в усьому іншому.
Для всіх інших: будь ласка, просто зрозуміймо, чому ми радимо завжди використовувати оператори з крапкою для поелементних обчислень: v .* v працює саме так, як можна очікувати, і дає [1, 4, 9].
Вибір елементів вектора за номером індексу обговорювався в попередніх концепціях.
a = collect('A':'Z') # => 26-element Vector{Char}
# index with an integer
a[2] # => 'B'
# index with a range
a[12:2:18] # => ['L', 'N', 'P, 'R']
# index with another vector
a[ [1, 3, 5] ] # => ['A', 'C', 'E']
Також можна вибирати елементи, які задовольняють певний логічний вираз (технічно, «предикат»). Зазвичай це потребує броадкастингу.
julia> a[a .< 'D']
3-element Vector{Char}:
'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
'B': ASCII/Unicode U+0042 (category Lu: Letter, uppercase)
'C': ASCII/Unicode U+0043 (category Lu: Letter, uppercase)
Для складніших виразів крапки зазвичай розмножуються (але вони маленькі й легко набираються).
julia> a[a .< 'D' .|| a .> 'W']
6-element Vector{Char}:
'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
'B': ASCII/Unicode U+0042 (category Lu: Letter, uppercase)
'C': ASCII/Unicode U+0043 (category Lu: Letter, uppercase)
'X': ASCII/Unicode U+0058 (category Lu: Letter, uppercase)
'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
'Z': ASCII/Unicode U+005A (category Lu: Letter, uppercase)
Нагадування: «вектор» насправді може бути будь-яким відповідним упорядкованим обʼєктом, який можна перебирати, наприклад діапазоном:
julia> n = 3:10
3:10
julia> n[isodd.(n)]
4-element Vector{Int64}:
3
5
7
9
Усередині предикат перетворюється на BitVector, який потім використовується як індекс.
julia> condition = a .< 'D'
26-element BitVector:
1
1
1
0
# display truncated
julia> a[condition]
3-element Vector{Char}:
'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
'B': ASCII/Unicode U+0042 (category Lu: Letter, uppercase)
'C': ASCII/Unicode U+0043 (category Lu: Letter, uppercase)