Vectorsのコンセプトでは、「配列はJulia言語の中心にあります」と述べました。そして、ベクトルは1次元配列です。
そう考えると、ベクトルに対して_何かをする_ための多様で強力な方法が、この言語にはたくさん用意されていると期待するのも当然でしょう。それが何を意味するにせよ。
用語について一言。この文書では「ベクトル」について多くを語りますが、その内容の多くはあらゆるイテラブル型にも当てはまります。範囲、タプル、集合など、さまざまな型です。
ごく単純な関数の中には、ベクトルを入力にとり、(1次元の入力に対して)スカラーの出力を返すものがあります。
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()はベクトルに似た入力を任意の個数受け取り、タプルのイテレーターを返します。
通常、入力の長さはすべて同じです。1つだけ短い場合、ほかの入力は最も短い長さに切り詰められます。_もしかすると_意図したとおりかもしれませんが、_たいていは_コードのバグです。
数値のベクトルがあり、各値から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は成功します。このドットが何をしているのかを理解する必要があります。
それは2つのことです。両者が組み合わさって、望む結果が得られます。
まず、任意の中置演算子の_前に_ドットを付けると、「この演算を各要素に個別に適用する」という意味になります。
同様に、関数名の_後に_ドットを付けると、その関数はスカラー入力用に書かれていても「ベクトル化」されます。
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]はメモリ上に残り、ガベージコレクションの対象になります。
その結果、元のベクトルに割り当てられたメモリを再利用する前の例に比べて、2倍のメモリを使うことになります。
同様に、同じサイズのベクトル同士をブロードキャストするときにも、関連する微妙な違いがあります。
たとえば、同じサイズの2つのベクトルvとwがあるとします。
v .= wは、vのメモリ位置にwのcopyを作ります。以降に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)