println("¡Hola 🌎!")¡Hola 🌎!
Nuestro objetivo es trabajar sobre algoritmos, por lo que cualquier lenguaje que pueda expresar todo lo computable, puede ser adecuado. Pero dado que nuestro enfoque será experimental, y nuestra metodología incluye medir la factibilidad y desempeño de cada algoritmo en términos reales, entonces necesitamos un lenguaje donde las instrucciones, los acceso a memoria, y la manipulación de la misma sea controlable. En este caso, y mediando con la fácilidad de aprendizaje y la productividad, este curso utiliza el lenguaje de programación Julia.1 Pero no hay porque preocuparse por aprender un nuevo lenguaje, el curso utiliza ejemplos en Julia y utiliza una variante de su sintaxis como pseudo-código, pero las actividades se esperan tanto en Julia como en Python.
Ambos lenguajes de programación son fáciles de aprender y altamente productivos. Python es un lenguaje excelente para realizar prototipos, o para cuando existen bibliotecas que resuelvan el problema que se este enfrentando. Por otro lado, cuando se necesita control sobre las operaciones que se estan ejecutando, o la memoria que se aloja, Python no es un lenguaje que nos permita trabajar en ese sentido. Julia esta diseñado para ser veloz y a la vez mantener el dinámismo que se espera de un lenguaje moderno, adicionalmente, es posible conocer los tipos de instrucciones que realmente se ejecutan, así como también es posible controlar la alojación de memoria, ya se mediante la utilización de patrones que así nos lo permitan, o mediante instrucciones que nos lo aseguren.
Este curso esta escrito en Quarto, y se esperan reportes de de tareas y actividades tanto en Quarto https://quarto.org como en Jupyter https://jupyter.org/. La mayoría de los ejemplos estarán empotrados en el sitio, y en principio, deberían poder replicarse copiando, pegando, y ejecutando en una terminal de Julia.
Es importante clarificar que este capítulo introducirá el lenguaje de programación Julia hasta el nivel que se requiere en este curso, ignorando una gran cantidad de capacidades que no son de interés para nuestro curso. Se recomienda al alumno interesado la revisión del manual y la documentación oficial para un estudio más profundo del lenguaje.
El código se suele organizar en scripts, módulos y paquetes. Cada uno de estos define tipos y funciones que interactuan para componer las soluciones deseadas.
El resto de esta unidad esta dedicada a precisar la sintaxis del lenguaje y anotaciones de importancia sobre su funcionamiento.
El sitio oficial recomienda el uso de juliaup, una herramienta que permite manejar diferentes versiones de Julia y mantenerlas actualizarlas.
https://julialang.org/install/
Las versiones de Julia siguen el paradigma de semantic versioning (semver), por lo que juliaup permite gestionarlas de manera simple y efectiva. La versión estable es la 1.10 y las más nuevas son la 1.11 y la 1.12.
También es posible usar Colab de Google con el kernel para Julia; este usar julia 1.11 y hasta el momento, es el único disponible.
Una vez instalado, se puede ejecutar un REPL de Julia en la terminal ejecutando
```{bash}
$ julia
```dado que instalamos con juliaup podemos mantener diferentes versiones, e.g.,
```{bash}
$ juliaup list
```que nos mostrará una larga lista de posibles channels o versiones de instalación
```{bash}
$ juliaup add 1.10
$ juliaup default 1.10
```estas instrucciones añadirán la versión 1.10 y la establecerá como versión o canal por omisión. Puedes llamar diferentes versiones ejecutando julia +channel como sigue:
```{bash}
$ julia +1.12
_
_ _ _(_)_ | Documentation: https://docs.julialang.org
(_) | (_) (_) |
_ _ _| |_ __ _ | Type "?" for help, "]?" for Pkg help.
| | | | | | |/ _` | |
| | |_| | | | (_| | | Version 1.12.1 (2025-10-17)
_/ |\__'_|_|_|\__'_| | Official https://julialang.org release
|__/ |
julia>
```Uno de los programas más comunes es el siguiente
println("¡Hola 🌎!")¡Hola 🌎!
Es posible usar Colab para reducir la complejidad de la instalación, ya que cuenta con un kernel de Julia. Como se mencionaba anteriormente, solo se soporta la versión 1.11 que es subóptima con las versiones de paquetes que usaremos más adelante. Adicionalmente, se tiene limitante de los recursos limitados que se nos proporcionen, en particular al momento de escribir estas notas, aunque los recursos que se otorgan suelen ser suficientes para pruebas, no lo son en otros ámbitos: solo se tienen 2 vcpus y tiempos de ejecución limitados.
Una vez instalado julia; debemos instalar el paquete IJulia que instalará todo lo necesario para correr Jupyter (ver la sección de Pkg al final de esta unidad para más información sobre paquetes). Una vez corriendo, se debe seleccionar crear un notebook específicando el kernel de Julia para utilizarlo.
Según sus posibilidades de equipo:
IJulia y ejecute Jupyter.Las funciones son centrales en Julia. Por ahora veremos la estructura y más adelante, definiremos algunas.
Para ejecutar una función se utiliza la sintaxis fun(arg), esta regresará un valor, que depende de la función misma y muchas veces del tipo que tenga arg. Si fueran dos argumentos fun(arg1, arg2), etc. También se soportan argumentos con nombre fun(arg1, arg2, ...; kwarg=val) (kwargs para nombrarlos de manera sintética). En este caso, los kwargs no influyen en los tipos de salida. Esto puede parecer extraño pero es debido a las decisiones de implementación relacionadas con el desempeño.
Las funciones se definen como sigue:
;, se debe tener en cuenta que los tipos de los argumentos nombrados no son utilizados para determinar si una función debe compilarse. Los argumentos nombrados pueden o no tener valores por omisión.
function y end, usando ‘=’ para definirla.
do...end) que se pasa como primer argumento a fun, e.g., es equivalente a fun(x->x^2).
El ámbito o scope de las variables en Julia es sintáctico, que significa que se hereda del código donde las funciones fueron definidas, y no dinámico (que se hereda desde dónde se ejecuta la función). Aunque es el comportamiento de la mayoría de los lenguajes modernos, es importante conocerlo sobre todo para la creación de cerraduras sintácticas en funciones.
Las expresiones son la forma más genérica de expresar el código en Julia, comprenden operaciones aritméticas, asignación y declaración de variables, definiciones de bloques de código, llamadas de funciones, entre otras.
Cada linea suele ser una expresión, a menos que se extienda por múltiples lineas por medio de un agrupador de código o datos, estos pueden ser
begin
...
end
let
...
end
(...)
[...]
[...]for el in col
...
end
while cond
...
end
if cond
...
elseif cond
...
else
...
endfunction fun(...)
...
end
try
...
catch
...
finally
...
endentre las más utilizadas; claramentre hay infinidad de formas de componerlas para formar los algoritmos que se esten escribiendo.
Los comentarios en Julia se hacen por linea o por bloque.
Para comentar una linea se usa el carácter hash # y define una linea comentada desde ese punto hasta el salto de linea
# los siguientes son comentarios de linea completos, por lo que no se imprimirán
#println(:hola === :hola)
#println(typeof(:hola))
println(Symbol("hola mundo")) # este sí se imprimirá, pero este comentario nohola mundo
Para comentar por bloque, dicho bloque se encierra entre #= ... =#
println("hola mundo!" #=Symbol("hola mundo")=#)hola mundo!
La documentación oficial se encuentra en https://docs.julialang.org; la cuál cubre el lenguaje y las bibliotecas estandar. Fuera de eso, habrá que ver los sitios y documentaciones de cada paquete, que casi siempre estan en github.
Actualmente los chatbots como ChatGPT y Gemini también pueden ser una buena fuente de información sobre el API y las formas de trabajo. Nota: siempre se debe corroborar la información, ya que suelen alucinar.
La manera empotrada de consultar la documentación sobre una función es con el prefijo ? en el REPL.
Nota: Las ligas que salen estan rotas ya que no se adjunta la documentación en este manuscrito.
?println("holi")println([io::IO], xs...)
Print (using print) xs to io followed by a newline. If io is not supplied, prints to the default output stream stdout.
See also printstyled to add colors etc.
julia> println("Hello, world")
Hello, world
julia> io = IOBuffer();
julia> println(io, "Hello", ',', " world.")
julia> String(take!(io))
"Hello, world.\n"
Esto también indica que debemos documentar nuestro código, en particular se hace de la siguiente forma
```{julia}
"""
fun(...)
Esta función es un ejemplo de documentación
"""
function fun(...)
...
end
```Las definiciones de variables tienen la sintaxis variable = valor; las variables comunmente comienzan con una letra o _, las letras pueden ser caracteres unicode, no deben contener espacios ni puntuaciones como parte del nombre; valor es el resultado de evaluar o ejecutar una expresión.
Los operadores más comunes son los aritméticos +, -, *, /, ÷, %, \, ^, con precedencia y significado típico. Existen maneras compuestas de modificar una variable anteponiendo el operador aritmético al simbolo de asignación, e.g., variable += valor, que se expande a variable = variable + valor. Esto implica que variable debe estar previamente definida previo a la ejecución.
Los operadores lógicos también tienen el significado esperado.
| operación | descripción |
|---|---|
a && b |
AND lógico |
a || b |
OR lógico |
a ⊻ b |
XOR lógico |
!a |
negación lógica |
a < b |
comparación a es menor que b |
a > b |
comparación a es mayor que b |
a <= b |
comparación a es menor o igual que b |
a >= b |
comparación a es mayor o igual que b |
a == b |
comparación de igualdad |
a === b |
comparación de igualdad (a nivel de tipo/memoria) |
a != b |
comparación de desigualdad |
a !== b |
comparación de desigualdad (a nivel de tipo/memoria) |
En particular && y || implementan corto circuito de código, por lo que pueden usarse para el control de que operaciones se ejecutan. Cuando se compara a nivel de tipo 0 (entero) será diferente de 0.0 (real).
También hay operadores lógicos a nivel de bit, los argumentos son enteros.
| operación | descripción |
|---|---|
a & b |
AND a nivel de bits |
a | b |
OR a nivel de bits |
a ⊻ b |
XOR a nivel del bits |
~a |
negación lógica a nivel de bits |
Los valores literales son valores explicitos que Julia permite para algunos tipos de datos, y que permiten definirlos de manera simple; permitiendonos escribir datos directamente en el código.
Los números enteros se definen sin punto decimal, es posible usar _ como separador y dar más claridad al código. Los enteros pueden tener 8, 16, 32, o 64 bits; por omisión, se empaquetan en variables del tipo Int (Int64). Los valores hexadecimales se interpretan como enteros sin signo, y además se empaquetan al número de bits necesario minimo para contener. El comportamiento para valores en base 10 es el de hexadecimal es congruente con un lenguaje para programación de sistemas.
a = 100
println((a, sizeof(a)))
b = Int8(100)
println((b, sizeof(b)))
c = 30_000_000
println((c, sizeof(c)))
d = 0xffff
println((d, sizeof(d)))(100, 8)
(100, 1)
(30000000, 8)
(0xffff, 2)
Existen números enteros de precisión 128 pero las operaciones al día de hoy no son implementadas de manera nativa por los procesadores; así mismo se reconocen números de punto flotante de precisión media Float16 pero la mayoría de los procesadores no tienen soporte nativo para realizar operaciones con ellos, aunque los procesadores de última generación si lo tienen.
Si la precisión esta en duda o el contexto lo amérita, deberá especificarlo usando el constructor del tipo e.g., Int8(100), UInt8(100), Int16(100), UInt16(100), Int32(100), UInt32(100), Int64(100), UInt64(100).
Los números de punto flotante tienen diferentes formas de definirse, teniendo diferentes efectos. Para números de precision simple, 32 bits, se definen con el sufijo f0 como 3f0. El sufijo e0 también se puede usar para definir precisión doble (64 bit). El cero del sufijo en realidad tiene el objetivo de colocar el punto decimal, en notación de ingeniería, e.g., \(0.003\) se define como \(3f-3\) o \(3e-3\), dependiendo del tipo de dato que se necesite. Si se omite sufijo y se pone solo punto decimal entonces se interpretará como precision doble. Los tipos son Float32 y Float64.
Los datos booleanos se indican mediante true y false para verdadero y falso, respectivamente.
Los caracteres son símbolos para índicar cadenas, se suelen representar como enteros pequeños en memoria. Se especifican con comillas simples 'a', 'z', '!' y soporta simbolos unicode '🤠'.
Las cadenas de caracteres son la manera de representar textos como datos, se guardan en zonas contiguas de memoria. Se especifican con comillas dobles y también soportan símbolos unicode, e.g., "hola mundo", "pato es un 🐷".
Julia guarda los símbolos de manera especial y pueden ser utilizados para realizar identificación de datos eficiente, sin embargo, no es buena idea saturar el sistema de manejo de símbolos por ejemplo para crear un vocabulario ya que no liberará la memoria después de definirlos ya que es un mecánismo diseñado para la representación de los programas, pero lo suficientemente robusto y bien definido para usarse en el diseño e implementación de programas de los usuarios.
En Julia existe la noción de símbolo, que es una cadena que además solo existe en una posición en memoria se usa el prefijo : para denotarlos.
println(:hola === :hola)
println(typeof(:hola))
println(Symbol("hola mundo"))true
Symbol
hola mundo
El control de flujo nos permite escoger que partes del código se ejecutaran como consecuencia de la evaluación de una expresión, esto incluye repeticiones.
Las condicionales son el control de flujo más simple.
"par"
Se puede ignorar la clausula else dando solo la opción de evaluar (2) si (1) es verdadero. Finalmente, note que la condicional es una expresión y devuelve un valor.
a = 10
if log10(a) == 1
"es 10"
end"es 10"
También pueden concatenarse múltiples expresiones condicionales con elseif como se muestra a continuación.
a = 9
if a % 2 == 0
println("divisible entre 2")
elseif a % 3 == 0
println("divisible entre 3")
else
println("no divisible entre 2 y 3")
enddivisible entre 3
Es común utilizar la sintaxis en Julia (short circuit) para control de flujo:
false
es divisible entre tres
Fnalmente, existe una condicional de tres vias expresion ? expr-verdadero : expr-falso
a = 9
println(a % 2 == 0 ? "es divisible entre dos" : "no es divisible entre dos")
println(a % 3 == 0 ? "es divisible entre tres" : "no es divisible entre tres")no es divisible entre dos
es divisible entre tres
Los ciclos son expresiones de control de flujo que nos permiten iterar sobre una colección o repetir un código hasta que se cumpla alguna condición. En Julia existen dos expresiones de ciclos:
for x in colección ...expresiones... end ywhile condición ...expresiones... endEn el caso de for, la idea es iterar sobre una colección, esta colección puede ser un rango, i.e., inicio:fin, inicio:paso:fin, o una colección como las tuplas, los arreglos, o cualquiera que cumpla con la interfaz de colección iterable del lenguaje.
for i in 1:5
println("1er ciclo: ", i => i^2)
end
for i in [10, 20, 30, 40, 50]
println("2do ciclo: ", i => i/10)
end1er ciclo: 1 => 1
1er ciclo: 2 => 4
1er ciclo: 3 => 9
1er ciclo: 4 => 16
1er ciclo: 5 => 25
2do ciclo: 10 => 1.0
2do ciclo: 20 => 2.0
2do ciclo: 30 => 3.0
2do ciclo: 40 => 4.0
2do ciclo: 50 => 5.0
Al igual que en otros lenguajes modernos, se define la variante completa o comprehensive for que se utiliza para transformar la colección de entrada en otra colección cuya sintaxis se ejemplifica a continuación:
a = [i => i^2 for i in 1:5]
println(a)[1 => 1, 2 => 4, 3 => 9, 4 => 16, 5 => 25]
También es posible definir un generador, esto es, un código que puede generar los datos, pero que no los generará hasta que se les solicite.
a = (i => i^2 for i in 1:5)
println(a)
println(collect(a))Base.Generator{UnitRange{Int64}, var"#5#6"}(var"#5#6"(), 1:5)
[1 => 1, 2 => 4, 3 => 9, 4 => 16, 5 => 25]
Otra forma de hacer ciclos de intrucciones es repetir mientras se cumpla una condición:
i = 0
while i < 5
i += 1
println(i)
end
i1
2
3
4
5
5
Una tupla es un conjunto ordenado de datos que no se puede modificar y que se desea esten contiguos en memoria, la sintaxis en memoria es como sigue:
end también se utiliza para indicar el último elemento en una colección ordenada.
NTuple{4, Int64}
Tuple{Int64, Float64, Float32}
Pair{Int64, Int64}
(2, 7, 30.0f0, 100, 200)
La misma sintaxis puede generar diferentes tipos de tuplas. En el caso NTuple{4, Int4} nos indica que el tipo maneja cuatro elementos de enteros de 64 bits, los argumentos entre {} son parametros que especifican los tipos en cuestión. En el caso de Tuple se pueden tener diferentes tipos de elementos. La tupla Pair es especial ya que solo puede contener dos elementos y es básicamente para embellecer o simplificar las expresiones; incluso se crea con la sintaxis key => value y sus elementos pueden accederse mediante dos campos nombrados.
Los arreglos son datos del mismo tipo contiguos en memoria, a diferencia de las tuplas, los elementos se pueden modificar, incluso pueden crecer o reducirse. Esto puede implicar que se alojan en zonas de memoria diferente (las tuplas se colocan en el stack y los arreglos en el heap, ver la siguiente unidad para más información). Desde un alto nivel, los arreglos en Julia suelen estar asociados con vectores, matrices y tensores, y un arsenal de funciones relacionadas se encuentran definidas en el paquete LinearAlgebra, lo cual esta más allá del alcance de este curso.
a y b.
ini:fin.
Vector{Int64}
Vector{Float64}
(2, 7, 30.0, [20.0, 30.0])
, en favor de la escritura por filas separadas por ;.
a es una matriz de 2x2.
: para indicar todos los elementos.
2×2 Matrix{Int64}:
2 3
5 7
2-element Vector{Int64}:
2
5
2-element Vector{Int64}:
2
3
Un diccionario es un arreglo asociativo, i.e., guarda pares llave-valor. Permite acceder de manera eficiciente al valor por medio de la llave, así como también verificar si hay una entrada dentro del diccionario con una llave dada. La sintaxis es como sigue:
a que mapea simbolos a enteros.
:b por 20.
:d => 4 al diccionario a.
:a.
Dict(:a => 1, :b => 20, :c => 3)
Dict(:a => 1, :b => 20, :d => 4, :c => 3)
Dict{Symbol, Int64} with 3 entries:
:b => 20
:d => 4
:c => 3
Es posible utilizar diferentes tipos siempre y cuando el tipo en cuestión defina de manera correcta la función hash sobre la llave y la verificación de igualdad ==.
Un conjunto se representa con el tipo Set, se implementa de manera muy similar al diccionario pero solo necesita el elemento (e.g., la llave). Como conjunto implementa las operaciones clasificación de operaciones de conjuntos
a.
a con una colección, no se modifica el conjunto a.
a.
true
Set([50, 20, 10, 30, 40])
Set([50, 20, 30, 40])
Set([20])
Set([50, 200, 20, 30, 40, 100])
Basta con escribir una linea de código en el REPL de Julia y esta se compilará y ejecutará en el contexto actual, usando el ámbito de variables. Esto es conveniente para comenzar a trabajar, sin embargo, es importante conocer el flujo de compilación para tenerlo en cuenta mientras se códifica, y así generar código eficiente. En particular, la creación de funciones y evitar la inestabilidad de los tipos de las variables es un paso hacia la generación de código eficiente. También es importante evitar el alojamiento de memoria dinámica siempre que sea posible. A continuación se mostrará el análisis de un código simple a diferentes niveles, mostrando que el lenguaje nos permite observar la generación de código, que últimadamente nos da cierto control y nos permite verificar que lo que se esta implementando es lo que se específica en el código. Esto no es posible en lenguajes como Python.
1.2100000000000002
CodeInfo( 1 ─ %1 = intrinsic Base.mul_float(x, y)::Float64 └── return %1 ) => Float64
En este código, se utiliza la estructa de agrupación de expresiones let...end. Cada expresión puede estar compuesta de otras expresiones, y casi todo es una expresión en Julia. La mayoria de las expresiones serán finalizadas por un salto de linea, pero las compuestas como let, begin, function, if, while, for, do, module estarán finalizadas con end. La indentación no importa la indentación como en Python, pero es aconsejable para mantener la legibilidad del código. La linea 2 define e inicializa la variable e; la linea 3 llama a la función println, que imprimirá el resultado de e*e en la consola. La función println esta dentro de la biblioteca estándar de Julia y siempre esta visible. La linea 4 es un tanto diferente, es una macro que toma la expresión e*e y realiza algo sobre la expresión misma, en particular @code_type muestra como se reescribe la expresión para ser ejecutada. Note como se hará una llamada a la función Base.mul_float que recibe dos argumentos y que regresará un valor Float64. Esta información es necesaria para que Julia pueda generar un código veloz, el flujo de compilación llevaría esta información a generar un código intermedio de Low Level Virtual Machine (LLVM), que es el compilador empotrado en Julia, el cual estaría generando el siguiente código LLVM (usando la macro @code_llvm):
; Function Signature: *(Float64, Float64) ; @ float.jl:497 within `*` define double @"julia_*_12156"(double %"x::Float64", double %"y::Float64") #0 { top: %0 = fmul double %"x::Float64", %"y::Float64" ret double %0 }
Este código ya no es específico para Julia, sino para la maquinaría LLVM. Observe la especificidad de los tipos y lo corto del código. El flujo de compilación requeriría generar el código nativo, que puede ser observado a continuación mediante la macro @code_native:
.text
.file "*"
.section .ltext,"axl",@progbits
.globl "julia_*_12272" # -- Begin function julia_*_12272
.p2align 4, 0x90
.type "julia_*_12272",@function
"julia_*_12272": # @"julia_*_12272"
; Function Signature: *(Float64, Float64)
; ┌ @ float.jl:497 within `*`
# %bb.0: # %top
#DEBUG_VALUE: *:x <- $xmm0
#DEBUG_VALUE: *:y <- $xmm1
push rbp
mov rbp, rsp
vmulsd xmm0, xmm0, xmm1
pop rbp
ret
.Lfunc_end0:
.size "julia_*_12272", .Lfunc_end0-"julia_*_12272"
; └
# -- End function
.type ".L+Core.Float64#12274",@object # @"+Core.Float64#12274"
.section .lrodata,"al",@progbits
.p2align 3, 0x0
".L+Core.Float64#12274":
.quad ".L+Core.Float64#12274.jit"
.size ".L+Core.Float64#12274", 8
.set ".L+Core.Float64#12274.jit", 128953772616176
.size ".L+Core.Float64#12274.jit", 8
.section ".note.GNU-stack","",@progbits
En este caso podemos observar código específico para la computadora que esta generando este documento, es posible ver el manejo de registros y el uso de instrucciones del CPU en cuestión.
Este código puede ser eficiente dado que los tipos y las operaciones son conocidos, en el caso que esto no puede ser, la eficiencia esta perdida. Datos no nativos o la imposibilidad de determinar un tipo causarían que se generará más código nativo que terminaría necesitanto más recursos del procesador. Una situación similar ocurre cuando se aloja memoria de manera dinámica. Siempre estaremos buscando que nuestro código pueda determinar el tipo de datos para que el código generado sea simple, si es posible usar datos nativos, además de no manejar o reducir el uso de memoría dinámica.
Las funciones serán una parte central de nuestros ejemplos, por lo que vale la pena retomarlas y dar ejemplos.
function f(x)
x^2
endf (generic function with 1 method)
Siempre regresan el valor de la última expresión; note como el tipo (y no solo el valor) de retorno depende del tipo de la entrada, e.g., si x es un entero entonces x^2 será un entero, pero si x es una matriz, x^2 será una matriz.
Hay valores opcionales y kwargs, ambas tienen características diferentes:
function f(x, t=1)
(x+t)^2
end
function g(x; t=1)
(x+t)^2
endg (generic function with 1 method)
struct Point
x::Float32
y::Float32
endLa idea suele ser que todo se use de manera armoniosa
"""
Calcula la norma de un vector representado
como un tupla
"""
function norm(u::Tuple)
s = 0f0
for i in eachindex(u)
s += u[i]^2
end
sqrt(s)
end
"""
Calcula la norma de un vector de 2 dimensiones
representado como una estructura
"""
function norm(u::Point)
sqrt(u.x^2 + u.y^2)
end
(norm((1, 1, 1, 1)), norm(Point(1, 1)))(2.0f0, 1.4142135f0)
Una matriz aleatoria de \(4 \times 6\) se define como sigue
A = rand(Float32, 4, 6)4×6 Matrix{Float32}:
0.8358 0.309711 0.258171 0.0793639 0.678398 0.59407
0.784485 0.350497 0.402035 0.224535 0.109351 0.270337
0.199536 0.570861 0.544277 0.10487 0.865695 0.688674
0.710535 0.936328 0.989251 0.0613883 0.153327 0.815167
Un vector aleatorio de 6 dimensiones sería como sigue:
x = rand(Float32, 4)4-element Vector{Float32}:
0.46093136
0.46436906
0.09765369
0.9613556
entonces podriamos multiplicar x con A como sigue:
y = x' * A1×6 adjoint(::Vector{Float32}) with eltype Float32:
1.4521 1.26141 1.30986 0.210105 0.595414 1.25028
y'6-element Vector{Float32}:
1.4520993
1.2614062
1.3098645
0.21010543
0.5954138
1.250279
También existen otras formas para realizarla, aunque no suelen ser la mejor idea si se tienen alternativas canónicas:
using LinearAlgebra
dot.(Ref(x), eachcol(A))6-element Vector{Float32}:
1.4520993
1.2614062
1.3098646
0.21010543
0.5954138
1.250279
Este ejemplo muestra la técnica broadcasting que aplica una función a una colección; se indica añadiendo un punto al final del nombre de la función. Adicionalmente, hay una serie de reglas que se deben seguir para el manejo de las colecciones. La función eachcol crea un iterador sobre cada columna de la matriz A y Ref(x), nos permite que el broadcasting reconozca al vector x como un único elemento en lugar de una colección de valores.
Dados dos vectores, cree las siguientes funciones:
El ecosistema de paquetes de Julia es una de sus mayores fortalezas, impulsado por usu gestor de paquetes Pkg, el cual viene integrado en el REPL y en la su instalación mínima; es muy robusto. Se encarga de la instalación y actualización de librerías, así como también garantiza la reproducibilidad de los proyectos. Cada ambiente de trabajo en Julia utiliza archivos como Project.toml y Manifest.toml para registrar la paquetería usada, así como las versiones necesarias de todas las dependencias.
Para entrar al modo Pkg desde cualquier sesión de Julia en el REPL se debe teclear corchete que cierra ].
El prompt del REPL cambiará:
| Modo | Prompt |
|---|---|
| Normal (Julia) | julia> |
| Modo Pkg | (@v1.10) pkg> |
Ahora se puede ver qué paquetes están instalados en tu entorno actual.
| Comando | Acción |
|---|---|
st o status |
Muestra la lista de todos los paquetes instalados y sus versiones específicas. |
Para añadir una paquete a tu entorno, usa el comando add.
| Comando | Acción |
|---|---|
add Paquete |
Descarga e instala el paquete. |
En el caso de que un paquete ya no sea necesario, se puede desinstalar con el comando rm (de remove).
| Comando | Acción |
|---|---|
rm Paquete |
Elimina el paquete del entorno actual. |
Finalmente, es posible actualizar paquetes de manera individual o colectiva usando el comando up.
| Comando | Acción |
|---|---|
up o update |
Actualiza todos los paquetes instalados a su última versión compatible. |
up Paquete o update Paquete |
Actualiza Paquete a su última versión compatible. |
El entorno o ambiente (environment) se puede especificar de manera global o por diretorio, y nos sirve para aislar las aplicaciones y no entrar en dificultades por versiones.
| Comando | Acción |
|---|---|
activate dir |
Activa el directorio dir como ambiente. |
Supongamos que nos comparten un proyecto escrito en julia, lo primero que debemos hacer es activar e instanciar el ambiente; la instanciación es como sigue
| Comando | Acción |
|---|---|
instantiate |
Se instalan todos los paquetes indicados por el ambiente. |
Muchas veces también cambiamos paquetes locales que requiren reactualizar el ambiente, eso se consigue con ] resolve que actualizará las nuevas dependencias que cambiaron.
Para volver al modo de ejecución de código normal de Julia, se debe presionar backspace.
El prompt cambiará de nuevo a julia> y podrás usar los paquetes que instalaste con el comando using.
using DataFrames, CSVesto traera el paquete al entorno en memoria haciendo accesibles sus métodos y estructuras públicas.
Pkg desde el modo normal de Julia (fuera del modo Pkg del REPL)Existe un paquete interno de las instalaciones de julia llamado Pkg que es el que maneja todo lo anterior, este puede ser utilizado como cualquier paquete. Basicamente tiene funciones similares a las del modo Pkg (con nombres completos).
Ejemplos:
julia> import Pkg
julia> Pkg.add("PlotlyLight")
julia> Pkg.add(["CSV", "DataFrames"])
julia> Pkg.rm("PlotlyLight")
julia> Pkg.update()
julia> Pkg.status() Ahora para el manejo de los ambientes:
julia> import Pkg
julia> Pkg.activate(".")
julia> Pkg.instantiate()
julia> Pkg.add("Statistics")La función include("nombre_archivo.jl") es el método más simple en Julia para organizar código en múltiples archivos. Su función es equivalente a copiar y pegar el contenido del archivo especificado directamente en la línea donde se llama a include.
Sirve para estructurar grandes scripts en archivos más pequeños y manejables; el código incluido se ejecuta en el mismo alcance (scope) donde se llamó a include. Si llamas a include en el alcance global, las funciones y variables definidas en el archivo incluido se vuelven globales. Si lo llamas dentro de un módulo, se vuelven parte de ese módulo.
Es simple, pero no proporciona aislamiento, y puede generar conflictos de nombres si no se usa de manera adecuada.
Por otro lado, los módulos permmiten crear espacios de nombres (namespaces) aislados y bien definidos, utiles para organizar proyectos grandes y complejos.
Un módulo actúa como una caja que encierra sus funciones y variables. Todo lo que se define dentro de un módulo es privado por defecto para evitar conflictos de nombres con código externo.
module MiCalculadora
# Esta función es PRIVADA
function interna(x)
return x * 2
end
# Esta función se hace PÚBLICA con 'export'
export sumar
function sumar(a, b)
return a + b
end
endPara que las funciones, tipos o constantes dentro de un módulo sean accesibles desde afuera, deben ser explícitamente exportadas utilizando la palabra clave export.
Los modulos pueden anidarse.
Para utilizar las funciones de un módulo en otro script o en el REPL, se usan dos comandos principales:
| Comando | Acción |
|---|---|
using NombreModulo |
Importa solo los símbolos que han sido exportados por el módulo. |
import NombreModulo |
Importa el módulo completo. Para usar sus funciones, debes prefijarlas (ej: NombreModulo.sumar(1, 2)). |
En la práctica, un paquete o un proyecto grande de Julia casi siempre usa tanto include como módulos. De esta manera, include ayuda a la organización de archivos, mientras que el bloque module garantiza que todo el código esté contenido en un espacio de nombres único y limpio, evitando colisiones.
En particular, los paquetes pueden verse como la preparación de un módulo para su distribución, indicando los paquetes que usan (dependencias) y sus versiones especificas para los cuales fueron diseñados. También suelen incluir documentación y pruebas unitarias.
Se recomienda utilizar la versión 1.10 o superior, y puede obtenerse en https://julialang.org/.↩︎