Julia

Algunos conceptos básicos

Author

David Gómez-Castro

Variables

Para crear una variable, le asignamos un valor. Julia detecta por defecto elemento introducido.

Podemos añadir enteros, reales, o vectores de manera sencilla

a = 1
1
@show typeof(a);
typeof(a) = Int64
b = 1.0
1.0
@show typeof(b);
typeof(b) = Float64

Aunque podemos especificar el tipo de float manualmente, por ejemplo un tipo float más corto

b2 = Float32(1.0)
@show b2, typeof(b2);
(b2, typeof(b2)) = (1.0f0, Float32)

Hay que ser cuidado con los valores máximos

@show typemax(Int64);
typemax(Int64) = 9223372036854775807

El tipo Float incluye Inf, de modo que

@show typemax(Float64);
typemax(Float64) = Inf

Pero tenemos una opción elegante para comprobar que

@show prevfloat(typemax(Float64));
prevfloat(typemax(Float64)) = 1.7976931348623157e308

Y con algunos compartamiento inesperados como integer overflow

@show typemax(Int64)
@show typemax(Int64) + 1;
typemax(Int64) = 9223372036854775807
typemax(Int64) + 1 = -9223372036854775808

Para evitarlo se puede usar el tipo BigInt, que no tiene máximo

Muy útil, por ejemplo, para

@show factorial(BigInt(30));
factorial(BigInt(30)) = 265252859812191058636308480000000

Si preguntamos quien es el máximo, tendremos un error

@show typemax(BigInt);
LoadError: MethodError: no method matching typemax(::Type{BigInt})
Closest candidates are:
  typemax(::Union{Dates.DateTime, Type{Dates.DateTime}}) at /Applications/Julia-1.8.app/Contents/Resources/julia/share/julia/stdlib/v1.8/Dates/src/types.jl:453
  typemax(::Union{Dates.Date, Type{Dates.Date}}) at /Applications/Julia-1.8.app/Contents/Resources/julia/share/julia/stdlib/v1.8/Dates/src/types.jl:455
  typemax(::Union{Dates.Time, Type{Dates.Time}}) at /Applications/Julia-1.8.app/Contents/Resources/julia/share/julia/stdlib/v1.8/Dates/src/types.jl:457
  ...

De forma similar existe el tipo BigFloat de números arbitrariamente grandes. Es necesario tener cuidado pues estos números son muy pesados de almacenar.

a = big(1.0)
@show typeof(a);
typeof(a) = BigFloat
@show typemax(BigFloat)
@show prevfloat(typemax(BigFloat));
typemax(BigFloat) = Inf
prevfloat(typemax(BigFloat)) = 5.875653789111587590936911998878442589938516392745498308333779606469323584389875e+1388255822130839282

Lidiamos también con strings (cadenas de cateres)

palabra = "hola"
@show typeof(palabra);
typeof(palabra) = String

Añadimos también vectores

c = [2.1,3.2]
2-element Vector{Float64}:
 2.1
 3.2

Y accedemos a la componentes

c[2]
3.2

Esta sintaxis también funciona con strings

palabra[3]
'l': ASCII/Unicode U+006C (category Ll: Letter, lowercase)

Nótese que el vector sabe qué tipo de elementos contiene


Podemos introducir vectores

[1, 2]
2-element Vector{Int64}:
 1
 2

y matrices

A = [1 2; 3 4]
2×2 Matrix{Int64}:
 1  2
 3  4

No se deben confundir vectores con matrices fila

[1 2]
1×2 Matrix{Int64}:
 1  2

y matrices columna

reshape([1,2],2,1)
2×1 Matrix{Int64}:
 1
 2

Vectores

En Julia los vectores son, por defecto, columnas

A*c
2-element Vector{Float64}:
  8.5
 19.1
c'*A
1×2 adjoint(::Vector{Float64}) with eltype Float64:
 11.7  17.0

Y multiplicar en sentido contrario producirá un error

c*A
LoadError: DimensionMismatch: matrix A has dimensions (2,1), matrix B has dimensions (2,2)

Concatenación

A diferencia de en C, se puede añadir elementos a un vector

a = [1,2]
append!(a,3)
3-element Vector{Int64}:
 1
 2
 3

En este caso la notación es más correcta que, por ejemplo, la notación de MATLAB a = [a,1]

Usando esta notación en Julia obtenemos lo que hemos pedimos:

a = [1,2]
a = [a,1]
2-element Vector{Any}:
  [1, 2]
 1

En Julia, un vector puede contener elementos de varios tipos


Se pueden concatenar vectores con ;

v = [1, 2]
[v;v]
4-element Vector{Int64}:
 1
 2
 1
 2

Nótese la diferencia con

v = [1 2]
[v;v]
2×2 Matrix{Int64}:
 1  2
 1  2

Para concatenar vectores y matrices podemos usar hcat y vcat

A = [1 2; 3 4]
@show hcat(A, A)
@show [A A];
hcat(A, A) = [1 2 1 2; 3 4 3 4]
[A A] = [1 2 1 2; 3 4 3 4]

Esta sintaxis es equivalente a [A A]

vcat(A, A)
@show [A; A]
[A; A] = [1 2; 3 4; 1 2; 3 4]
4×2 Matrix{Int64}:
 1  2
 3  4
 1  2
 3  4

Asignaciones de vectores

Recordamos que en C, o en MATLAB, el siguiente código

int a = 1
int b = a
a = 2

Se queda en estado a=2 y b=1


En Julia (y en Python) también es así asignaciones completas tanto en escalares

a = 1
b = a
a = 2
@show a,b;
(a, b) = (2, 1)

como en vectores

a = [1,1]
b = a
a = [2,1]
@show a,b;
(a, b) = ([2, 1], [1, 1])

Pero si modificamos una componente

a = [1,1]
b = a
a[1] = 2
@show a,b;
(a, b) = ([2, 1], [2, 1])

Esto se llama “pass-by-sharing”. Es el comportamiento por defecto de las funciones en Julia. Apéndice

Si queremos evitarlo debemos usar la opción copy

a = [1.0,1.0]
b = copy(a)
a[1] = 2.0
@show a,b;
(a, b) = ([2.0, 1.0], [1.0, 1.0])

Asignación puntual de vectores

a = [1 2] 
b = [3 4]
c = b
c = a 
@show b; 
b = [3 4]

Primero compartimos b en c, y luego a en c. 

La notación .= asigna puntualmente:

a = [1 2] 
b = [3 4]
c = b
c .= a 
@show b; 
b = [1 2]

Hecha una primera introducción, es recomendable revisar la documentación de Julia

Documentación

Tipos abstractos

Funciones

Podemos hacer funciones escalares

a = 2.0; 
a^2
4.0
@show sqrt(a) , exp(a) , sin(a) ;
(sqrt(a), exp(a), sin(a)) = (1.4142135623730951, 7.38905609893065, 0.9092974268256817)

Y todas estas funciones se pueden aplicar puntualmente a vectores o matrices, indicando que la operación es puntual

v = [1,2];
v.^2
2-element Vector{Int64}:
 1
 4
@show sqrt.(v) , exp.(v) , sin.(v) ;
(sqrt.(v), exp.(v), sin.(v)) = ([1.0, 1.4142135623730951], [2.718281828459045, 7.38905609893065], [0.8414709848078965, 0.9092974268256817])

El usuario puede también definir funciones.

Bien de la manera convencional

function suma(x,y)
    x+y
end

suma(1,2)
3

O en línea

suma(x,y) = x+y

suma(1,2)
3

Como es natural, esta función no se comportará de manera natural si le pasamos texto

suma("hola, ", "majo")
"hola, majo"

Sin embargo, podemos hacer la función comportarse distinto según el tipo de entradas que tenga. Por ejemplo, es “natural” que suma concatene cadenas de texto

suma(x::String,y::String) = x * y
suma("aquí ", "estoy")
"aquí estoy"

Pero sigue haciendo lo que debe con números

suma(1,2)
3

Volveremos sobre esta idea más adelante.

Parámetros opcionales

function g(x ; n=0 , m=1)
    if n==0
        return x
    else
        return x+m
    end
end 
@show g(1)
@show g(1;n=1)
@show g(1;n=1,m=2)
g(1) = 1
g(1; n = 1) = 2
g(1; n = 1, m = 2) = 3
3

Hay que ser cuidadoso con los argumentos de funciones.

Mutabilidad en Julia

De acuerdo con la documentación de Julia:

Julia function arguments follow a convention sometimes called “pass-by-sharing”, which means that values are not copied when they are passed to functions. Function arguments themselves act as new variable bindings (new locations that can refer to values), but the values they refer to are identical to the passed values. Modifications to mutable values (such as Arrays) made within a function will be visible to the caller. This is the same behavior found in Scheme, most Lisps, Python, Ruby and Perl, among other dynamic languages.


Las variables de tipo Float e Int no cambian dentro de funciones

function AumentarUno(a) 
    a += 1.0;
    return a
end
a = 2.0
display("Primero a = $a.")
display("Calculamos AumentarUno(a) = $(AumentarUno(a))")
display("Ahora a = $a")
"Primero a = 2.0."
"Calculamos AumentarUno(a) = 3.0"
"Ahora a = 2.0"

Se suele decir que Float64 es immutable, pues modificarlos dentro de funciones no afecta a su valor exterior


Los vectores y matrices, sin embargo, son mutable.

Veamos en este ejemplo

function AumentarUnoVectorial(v) 
    v = v .+ 1;
    return v
end;

v = [1,2];

@show v
@show AumentarUnoVectorial(v)
@show v;
v = [1, 2]
AumentarUnoVectorial(v) = [2, 3]
v = [1, 2]

El motivo de esto es que los vectores y matrices suelen ser estructuradas pesadas, y no es recomendable copiarlas por defecto.


La convención es añadir ! al final de la función si modifica su argumento.

Así, el nombre correcto de la función es AumentarUnoVectorial!

Para evitar esta comportamiento, que es por defecto, se puede pasar una copia a la función. Por ejemplo

function AumentarUnoVectorial!(v) 
    v .= v .+ 1;
    return v
end

v = [1,2];
@show v
@show AumentarUnoVectorial!(copy(v))
@show v;
v = [1, 2]
AumentarUnoVectorial!(copy(v)) = [2, 3]
v = [1, 2]

Finalmente cabe resaltar que hay que tener cuidado con el orden de las operaciones en display.

Primero se hacen todos los cálculos, y luego se muestran. Por ejemplo

v = [1,2];
@show v
@show AumentarUnoVectorial!(v)
@show v;
v = [1, 2]
AumentarUnoVectorial!(v) = [2, 3]
v = [2, 3]

Lógica

Podemos hacer comprobaciones lógicas sencillas

a = 1; b = 2;
a == b
false
@show a < b, a <= b, a != b;
(a < b, a <= b, a != b) = (true, true, true)

If/then/else

if x  1 
    x 
else 
    x+1 
end

La sintaxis if/then/else tiene una sintaxis abreviada

g(n) = x  1 ? x : x+1
if x  1 
    x 
elseif x  2 
    x+1 
else 
    x + 2
end

Bucles

En Julia podemos crear bucles de tipo for y while.

La sintaxis 1:3 corresponde al vector [1,2,3]

for i=1:3
    @show i
end
i = 1
i = 2
i = 3
i=1
while i<=3
    @show i
    i = i+1;
end
i = 1
i = 2
i = 3

Y hay algunos comportamientos avanzados

x = [4.0,5.1,6.0]

for (index, value) in enumerate(x)
    
    display("El valor de x en el índice $index es $value")
    
end
"El valor de x en el índice 1 es 4.0"
"El valor de x en el índice 2 es 5.1"
"El valor de x en el índice 3 es 6.0"

Ejercicio.

Calcular los cinco primeros términos de la sucesión de Fibonacci \[a_0 = 1, \qquad \qquad a_1 = 1, \qquad \qquad a_{n} = a_{n-1} + a_{n-2}.\] Almacenarlos en un vector.


N = 5;
Fib = Vector{Int}(undef, N)
Fib[1] = 1
Fib[2] = 1
for n=3:N
    Fib[n] = Fib[n-1] + Fib[n-2];
end 
display(Fib)
5-element Vector{Int64}:
 1
 1
 2
 3
 5

Una formulación “mágica”

fib(n::Integer) = n  2 ? one(n) : fib(n-1) + fib(n-2)
@show fib(5);
fib(5) = 5

Ejercicio.

Calcular los términos de la sucesión de Fibonacci hasta el primero que sea mayor que 25.

Almacenarlos en un vector.


Fib = [1,1]
while Fib[end] <= 25
    append!(Fib, Fib[end] + Fib[end-1]);
end 
Fib
9-element Vector{Int64}:
  1
  1
  2
  3
  5
  8
 13
 21
 34

Sin embargo, es preferible reservar el tamaño de memoria suficiente para la variable.

function Fibonacci1(N)
    Fib = Vector{BigInt}(undef, N)
    Fib[1] = 1
    Fib[2] = 1
    for n=3:N
        Fib[n] = Fib[n-1] + Fib[n-2];
    end 
    return Fib[N]
end
@time aN = Fibonacci1(200);
  0.000025 seconds (401 allocations: 12.055 KiB)
function Fibonnaci2(M)
    Fib = [BigInt(1), BigInt(1)]
    while Fib[end] < M
        append!(Fib, Fib[end] + Fib[end-1]);
    end 
end
@time Fibonnaci2(aN)
  0.006980 seconds (10.03 k allocations: 490.464 KiB, 99.35% compilation time)

Metaprogramación y macros

Legado de Lisp

The strongest legacy of Lisp in the Julia language is its metaprogramming support. Like Lisp, Julia represents its own code as a data structure of the language itself. Since code is represented by objects that can be created and manipulated from within the language, it is possible for a program to transform and generate its own code. This allows sophisticated code generation without extra build steps, and also allows true Lisp-style macros operating at the level of abstract syntax trees. In contrast, preprocessor “macro” systems, like that of C and C++, perform textual manipulation and substitution before any actual parsing or interpretation occurs. Because all data types and code in Julia are represented by Julia data structures, powerful reflection capabilities are available to explore the internals of a program and its types just like any other data.

La vida de un string

prog = "1 + 1"

ex1 = Meta.parse(prog)
:(1 + 1)
typeof(ex1)
Expr
eval(ex1) 
2

ex1.head
:call
ex1.args
3-element Vector{Any}:
  :+
 1
 1
ex2 = Expr(:call, :+, 1, 1)
ex1 == ex2
true

Macros

macro sayhello()
           return :( println("Hello, world!") )
       end
@sayhello (macro with 2 methods)
@sayhello()
Hello, world!
macro sayhello(name)
           return :( println("Hello, ", $name) )
       end
@sayhello (macro with 2 methods)
@sayhello("human")
Hello, human

FIN