No Conceito Vectors, dissemos que "arrays estão no coração da linguagem Julia" e que um vetor é um array unidimensional.
Diante disso, é razoável esperar que a linguagem ofereça muitas formas versáteis e poderosas de fazer coisas com vetores, seja lá o que isso signifique.
Uma observação sobre terminologia: embora este documento fale muito de "vetores", boa parte dele também vale para qualquer tipo iterável: intervalos, tuplas, conjuntos e vários outros.
Algumas funções bem simples recebem um vetor como entrada e (para entradas unidimensionais) retornam uma saída escalar.
v = [2, 3, 4]
length(v) # => 3
sum(v) # => 9
Quando chegarmos ao Conceito sobre arrays multidimensionais, vai ficar mais claro que isso é redução de dimensão, e não necessariamente o retorno de um escalar. Se isso não fizer sentido para você, não se preocupe com isso por enquanto.
Existem muitas outras funções desse tipo.
Veja o Conceito Statistics para alguns exemplos.
Também existem funções que operam sobre vários vetores, como a (muito útil) 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() recebe um número arbitrário de entradas do tipo vetor e retorna um iterador de tuplas.
Normalmente, todas as entradas têm o mesmo comprimento. Se uma delas for mais curta, as outras são truncadas para o comprimento da mais curta: talvez seja o que você queria, mas, mais comumente, é um bug no seu código.
Suponha que você tenha um vetor numérico e queira subtrair 0.5 de cada valor.
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)
Isso falha, então e se fizermos a subtração com outro vetor?
julia> v - [0.5, 0.5, 0.5]
3-element Vector{Float64}:
0.7
1.0
1.2
Funciona, mas é bem tedioso e consome muita memória conforme os vetores ficam maiores.
Dependendo de até onde você chegou no programa de estudos, você provavelmente consegue pensar em outras abordagens:
[x - 0.5 for x in v] dá o resultado desejado (estilo Python).map(x -> x - 0.5, v) também funciona (estilo Haskell, embora seja comum em muitas linguagens).Felizmente, Julia tem um ponto "mágico" que resolve esse problema de forma bem simples: v .- 0.5 é tudo o que você precisa.
A próxima seção explica por quê.
Então, v - 0.5 falha, mas v .- 0.5 funciona, e precisamos entender o que esse ponto está fazendo.
Duas coisas, que se combinam para dar o resultado desejado.
Primeiro, adicionar um ponto antes de qualquer operador infixo significa "aplicar essa operação a cada elemento separadamente".
Do mesmo modo, adicionar um ponto depois do nome de uma função a "vetoriza", mesmo que a função tenha sido escrita para entradas escalares.
julia> sqrt.([1, 4, 9])
3-element Vector{Float64}:
1.0
2.0
3.0
Como um parêntese, operadores infixos são, na verdade, apenas açúcar sintático para a função subjacente.
Isso significa que, por exemplo, o intérprete traduz [1, 5, 10] .% 3 para mod.([1, 5, 10], 3), e então a sintaxe mod. é executada (as duas versões retornam [1, 2, 1]).
Vimos em um exemplo anterior que podemos subtrair vetores de mesmo comprimento, mas entenda que .- é um operador mais seguro do que -, porque deixa clara a intenção de operar elemento a elemento.
julia> v .- [0.5, 0.5, 0.5]
3-element Vector{Float64}:
0.7
1.0
1.2
E no caso de vetores de comprimentos diferentes?
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
Em geral, comprimentos diferentes geram um erro, exceto quando um deles tem comprimento 1 (tecnicamente, uma dimensão "singleton").
Singletons como [0.5,] ou simplesmente 0.5 são expandidos automaticamente até o comprimento necessário por meio de repetição.
É nisso que consiste o broadcasting.
Quem estiver preocupado com o uso de memória por causa dessa "repetição" pode ficar tranquilo: ela é implementada de uma forma muito eficiente que, na prática, não copia os valores na memória.
Quem já conhece broadcasting em outras linguagens deve notar que a abordagem de Julia é (em grande parte) parecida com a do NumPy, mas bem menos tolerante a incompatibilidades de tamanho do que a do R.
Se o uso de memória é uma preocupação, as operações in-place são uma forma comum de tentar reduzir alocações.
No entanto, as operações de broadcasting dos exemplos acima criam um novo Vector em vez de modificar o original.
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
Para modificar v in-place, a modificação precisa ser aplicada com broadcasting.
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
Mas atenção! O ponto antes do operador de atribuição .= faz diferença.
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
Parece que funcionou do mesmo jeito, mas aqui v .+ 1 criou um novo vetor [2, 3, 4] e depois o atribuiu à variável v, deixando o vetor inicial [1, 2, 3] na memória para ser coletado pelo garbage collector.
No fim, isso usa o dobro de memória do que o exemplo anterior, que reaproveita a memória alocada para o vetor inicial.
Da mesma forma, há sutilezas parecidas ao fazer broadcasting com vetores do mesmo tamanho.
Por exemplo, dados dois vetores v e w do mesmo tamanho:
v .= w produz uma copy de w no local de memória de v. Mudanças posteriores em v não afetam w, e vice-versa.v = w produz outro ponteiro para o local de memória de w, com o nome v. Mudanças posteriores em v se refletem em w, e vice-versa.Esta subseção é bastante pesada em matemática, então não se espera que a maioria dos estudantes entenda tudo de verdade. Ainda assim, é um aviso útil que pode ajudar a depurar quando você vir mensagens de erro inesperadas.
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
Se você por acaso tem formação em álgebra linear, então (1) você não é um usuário típico do Exercism (mas você é muito bem-vindo aqui!) e (2) talvez reconheça que v é um vetor coluna, u é um vetor linha, u * v é o produto interno e v * u é o produto externo.
Julia segue as regras da matemática, nisto como em tudo.
Para todos os outros: entenda apenas por que recomendamos que você use sempre operadores com ponto para cálculos elemento a elemento: v .* v funciona exatamente como você esperaria, dando [1, 4, 9].
A seleção de elementos de um vetor pelo número do índice já foi discutida em Conceitos anteriores.
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']
Também é possível selecionar elementos que satisfazem alguma expressão lógica (tecnicamente, um "predicado"). Isso geralmente exige broadcasting.
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)
Em expressões mais complexas, os pontos tendem a se multiplicar (mas são pequenos e fáceis de digitar).
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)
Lembrete: o "vetor" pode, na verdade, ser qualquer iterável ordenado adequado, como um intervalo:
julia> n = 3:10
3:10
julia> n[isodd.(n)]
4-element Vector{Int64}:
3
5
7
9
Internamente, o predicado é convertido em um BitVector, que então é usado como índice.
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)