En el concepto Vectors, decíamos que «los arrays son el corazón del lenguaje Julia» y que un vector es un array unidimensional.
Dicho 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 terminología: aunque este documento habla mucho de «vectores», gran parte de lo que cuenta también se aplica a cualquier tipo iterable: rangos, tuplas, conjuntos y otros cuantos.
Algunas funciones muy sencillas reciben un vector como entrada y (para una entrada unidimensional) devuelven un escalar como salida.
v = [2, 3, 4]
length(v) # => 3
sum(v) # => 9
Cuando lleguemos al concepto sobre arrays multidimensionales, quedará más claro que esto es reducción de dimensiones y no necesariamente la devolución de un escalar. Si eso no tiene ningún sentido para ti, no te preocupes por ello por ahora.
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() toma un número arbitrario de entradas similares a vectores y devuelve un iterador de tuplas.
Lo habitual es que las entradas tengan todas la misma longitud. Si una es más corta, las demás se truncan a la longitud de la más corta: puede que sea lo que pretendías, pero lo más frecuente es que sea un bug en tu código.
Supongamos que tienes un vector numérico y quieres restar 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, así que ¿y si restamos otro vector?
julia> v - [0.5, 0.5, 0.5]
3-element Vector{Float64}:
0.7
1.0
1.2
Funciona, pero resulta bastante tedioso y consume mucha memoria a medida que los vectores se alargan.
Según lo lejos que hayas llegado en el temario, probablemente se te ocurran otros enfoques:
[x - 0.5 for x in v] da el resultado deseado (al estilo de Python).map(x -> x - 0.5, v) también funciona (al estilo de Haskell, aunque es habitual en muchos lenguajes).Por suerte, Julia tiene un punto «mágico» que resuelve este problema de forma muy sencilla: v .- 0.5 es todo lo que necesitas.
La siguiente sección explica por qué.
Así que v - 0.5 falla pero v .- 0.5 funciona, y tenemos que entender qué hace el punto.
Son dos cosas, que se combinan para dar el resultado deseado.
En primer lugar, poner un punto delante de cualquier operador infijo significa «aplica esta operación a cada elemento por separado».
Del mismo modo, poner un punto después del nombre de una función la «vectoriza», incluso si la función se escribió para entradas escalares.
julia> sqrt.([1, 4, 9])
3-element Vector{Float64}:
1.0
2.0
3.0
Como apunte, los operadores infijos no son más que azúcar sintáctico para la función subyacente.
Esto significa que, por ejemplo, el intérprete traduce [1, 5, 10] .% 3 a mod.([1, 5, 10], 3), y la sintaxis mod. se ejecuta a continuación (ambas versiones devuelven [1, 2, 1]).
En un ejemplo anterior vimos que podemos restar vectores de la misma longitud, aunque conviene entender que .- es un operador más seguro que -, porque deja clara la intención de operar elemento a elemento.
julia> v .- [0.5, 0.5, 0.5]
3-element Vector{Float64}:
0.7
1.0
1.2
¿Y 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, las longitudes distintas son un error, excepto cuando una tiene longitud 1 (técnicamente, una dimensión «singleton»).
Los singleton como [0.5,] o simplemente 0.5 se expanden automáticamente a la longitud necesaria por 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 memoria.
Los programadores familiarizados con el broadcasting en otros lenguajes deben tener en cuenta que el enfoque de Julia es (en su mayor parte) 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 in situ son una forma habitual de intentar reducir las asignaciones.
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 in situ, hay que hacer broadcasting de la modificación.
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 cuidado! El punto delante 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 que ha funcionado igual, pero aquí v .+ 1 ha creado un nuevo vector [2, 3, 4] y luego lo ha asignado a la variable v, dejando el vector inicial [1, 2, 3] en memoria hasta que lo recoja el recolector de basura.
Esto acaba usando el doble de memoria que el ejemplo anterior, que reutiliza la memoria asignada al vector inicial.
Del mismo modo, hay sutilezas relacionadas cuando se hace 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, ni 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 es bastante matemática, así que no se espera que la mayoría de los estudiantes la entienda de verdad. 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 eres muy bienvenido aquí!) y (2) puede que reconozcas que v es un vector columna, u es un vector fila, u * v es el producto interno y v * u es el producto exterior.
Julia sigue las reglas de las matemáticas, en esto como en todo.
Para todos los demás: basta con que entiendas por qué recomendamos usar siempre operadores con punto para los cálculos elemento a elemento: v .* v funciona exactamente como cabría esperar, y da [1, 4, 9].
La selección de elementos de un vector por su número de índice ya 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 los elementos que cumplen cierta expresión lógica (técnicamente, un «predicado»). Esto normalmente 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)
En expresiones más complejas, los puntos tienden a multiplicarse (pero son pequeños y fáciles de teclear).
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)
Un recordatorio: el «vector» puede ser, de hecho, cualquier iterable ordenado que sea adecuado, como un rango:
julia> n = 3:10
3:10
julia> n[isodd.(n)]
4-element Vector{Int64}:
3
5
7
9
Internamente, el predicado se convierte en un BitVector que después 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)