En el concepto Vectors dijimos que «los arrays son el corazón del lenguaje Julia» y que un vector es un array de una dimensión.
Dado esto, es razonable esperar que el lenguaje ofrezca muchas formas versátiles y potentes de hacer cosas con vectores, sea lo que sea que eso signifique.
Una nota sobre la terminología: aunque este documento habla mucho de «vectores», gran parte también se aplica a cualquier tipo iterable: rangos, tuplas, conjuntos y varios más.
Algunas funciones muy simples reciben un vector como argumento y (para argumentos de una dimensión) devuelven un escalar.
v = [2, 3, 4]
length(v) # => 3
sum(v) # => 9
Cuando lleguemos al concepto sobre arrays multidimensionales, quedará más claro que esto es una reducción de dimensiones y no necesariamente el resultado de devolver un escalar. Si esto no tiene sentido para ti, por ahora no te preocupes.
Hay muchas más funciones de este tipo.
Consulta el concepto Statistics para ver algunos ejemplos.
También hay funciones que operan sobre varios vectores, como la (muy ú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() recibe una cantidad arbitraria de argumentos tipo vector y devuelve un iterador de tuplas.
Por lo general, todos los argumentos tienen la misma longitud. Si uno es más corto, los demás se truncan a la longitud más corta: quizás lo que querías, pero lo más común es que sea un bug en tu código.
Supón que tienes un vector numérico y quieres restarle 0.5 a 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)
Eso falla, entonces ¿qué tal si restamos otro vector?
julia> v - [0.5, 0.5, 0.5]
3-element Vector{Float64}:
0.7
1.0
1.2
Funciona, pero es bastante tedioso y consume mucha memoria a medida que los vectores se hacen más largos.
Dependiendo de qué tan lejos hayas llegado en el temario, probablemente se te ocurran otros enfoques:
[x - 0.5 for x in v] da el resultado deseado (estilo Python).map(x -> x - 0.5, v) también funciona (estilo Haskell, aunque es común en muchos lenguajes).Por suerte, Julia tiene un punto «mágico» que resuelve este problema de forma muy simple: v .- 0.5 es todo lo que necesitas.
La siguiente sección explica por qué.
Entonces, v - 0.5 falla pero v .- 0.5 funciona, y necesitamos entender qué hace el punto.
Dos cosas, que se combinan para dar el resultado deseado.
En primer lugar, agregar un punto antes de cualquier operador infijo significa «aplicar esta operación a cada elemento por separado».
De manera similar, agregar un punto después del nombre de una función la «vectoriza», incluso si la función se escribió para argumentos escalares.
julia> sqrt.([1, 4, 9])
3-element Vector{Float64}:
1.0
2.0
3.0
Como apunte, los operadores infijos en realidad son solo azúcar sintáctica para la función subyacente.
Esto significa que, por ejemplo, [1, 5, 10] .% 3 se traduce a mod.([1, 5, 10], 3) por parte del intérprete, y luego la sintaxis mod. se ejecuta (ambas versiones devuelven [1, 2, 1]).
En un ejemplo anterior vimos que podemos restar vectores de igual longitud; sin embargo, ten en cuenta que .- es un operador más seguro que -, porque deja clara la intención de operar elemento por elemento.
julia> v .- [0.5, 0.5, 0.5]
3-element Vector{Float64}:
0.7
1.0
1.2
¿Y qué pasa con vectores de distinta longitud?
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
En general, que las longitudes sean distintas es un error, excepto cuando una tiene longitud 1 (técnicamente, una dimensión «singleton»).
Los singletons como [0.5,] o simplemente 0.5 se expanden automáticamente a la longitud necesaria mediante repetición.
Esto es el corazón del broadcasting.
Quien se preocupe por el uso de memoria que implica esta «repetición» puede quedarse tranquilo: está implementada de una forma muy eficiente que en realidad no copia los valores en la memoria.
Quienes programan y conocen el broadcasting de otros lenguajes deben tener en cuenta que el enfoque de Julia es (en su mayoría) similar al de NumPy, pero mucho menos tolerante con las discrepancias de tamaño que R.
Si el uso de memoria te preocupa, las operaciones en el lugar son una forma habitual de intentar reducir las asignaciones de memoria.
Sin embargo, las operaciones de broadcasting de los ejemplos anteriores crean un nuevo Vector en lugar de modificar el 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 en el lugar, la modificación debe hacerse con 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
¡Pero ten cuidado! El punto antes del operador de asignación .= es importante.
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 haber funcionado de la misma manera, pero aquí v .+ 1 creó un nuevo vector [2, 3, 4] y luego lo asignó a la variable v, dejando el vector inicial [1, 2, 3] en memoria para que el recolector de basura lo libere.
Esto termina usando el doble de memoria que el ejemplo anterior, que reutiliza la memoria reservada para el vector inicial.
Asimismo, hay sutilezas relacionadas cuando haces broadcasting con vectores del mismo tamaño.
Por ejemplo, dados dos vectores v y w del mismo tamaño:
v .= w produce una copy de w en la ubicación de memoria de v. Los cambios posteriores en v no afectan a w, y viceversa.v = w produce otro puntero a la ubicación de memoria de w con el nombre v. Los cambios posteriores en v se reflejan en w, y viceversa.Esta subsección tiene bastante contenido matemático, así que no se espera que la mayoría de los estudiantes la entienda del todo. Sin embargo, es una advertencia útil que puede ayudarte a depurar cuando veas mensajes de error inesperados.
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
Si resulta que tienes formación en álgebra lineal, entonces (1) no eres un usuario típico de Exercism (¡pero nos encanta que estés aquí!) y (2) quizás reconozcas que v es un vector columna, u es un vector fila, u * v es el producto interno y v * u es el producto externo.
Julia sigue las reglas de las matemáticas, en esto como en todo.
Para los demás: solo entiende por qué recomendamos que siempre uses operadores con punto para los cálculos elemento por elemento: v .* v funciona exactamente como esperarías, y da [1, 4, 9].
La selección de elementos de un vector por número de índice se ha tratado en conceptos 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']
También es posible seleccionar elementos que cumplen cierta expresión lógica (técnicamente, un «predicado»). Por lo general, esto requiere 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)
Para expresiones más complejas, los puntos tienden a multiplicarse (pero son pequeños y fáciles de escribir).
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)
Recordatorio: el «vector» en realidad puede ser cualquier iterable ordenado apropiado, como un rango:
julia> n = 3:10
3:10
julia> n[isodd.(n)]
4-element Vector{Int64}:
3
5
7
9
De forma interna, el predicado se convierte en un BitVector, que luego se usa 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)