Un día con el lenguaje de programación Julia

Julia

Julia es un lenguaje de alto nivel, como R o Python, que permite escribir código de manera fácil y rápida, pero con una velocidad de ejecución similar a la de C, C++ o Fortran (~ 2x)


In [7]:
println("Hola mundo")


Hola mundo

In [4]:
using DataFrames
benchmark = readtable("../data/benchmark.csv", separator=';')


Out[4]:
BenchmarkFortranJuliaPythonRMatlabOctaveMathematicaJavaScriptGoLuaJITJava
1fib0.72.1177.76533.5226.899324.35118.533.361.861.711.21
2parse_int5.051.4517.0245.73802.529581.4415.026.061.25.773.35
3quicksort1.311.1532.89264.544.921866.0143.232.71.292.032.6
4mandel0.810.7915.3253.167.58451.815.130.661.110.671.35
5pi_sum1.01.021.999.561.0299.311.691.011.01.01.0
6rand_mat_stat1.451.6617.9314.5614.5230.935.952.32.963.273.92
7rand_mat_mul3.481.021.141.571.121.121.315.071.421.162.36

In [11]:
using Plots
scatter(benchmark, :Benchmark, [:Julia, :Fortran, :Python, :R], yscale=:log10, ylab="Tiempo relativo a C")


Out[11]:

Julia fue diseñado para ser rápido

Julia fue diseñado desde su creación para ser un lenguaje de programación compilado en tiempo real (JIT por just-in-time).

  • No es necesario recurrir a código en C para obtener velocidad.

  • Los tipos de datos y métodos/funciones definidos por el usuario son tan rápidos como los que vienen en la biblioteca estándar. De hecho… La biblioteca estándar de Julia está escrita en Julia (es un lenguaje homoicónicos).

El secreto de Julia es que compila cada función de la manera más eficiente y específica para cada tipo de dato sobre la cual se usa o se define.

Cuando más de un método existe con el mismo nombre, Julia elige el método más específico para el tipo de sus argumentos, debido a su diseño basado en multiple dispatch.

La primera vez que se llama a una función, su tiempo de ejecución es más lento porque incluye el tiempo de compilación. Pero la segunda vez que se ejecuta, es casi tan rápida como si corriera en C (dado que ya fue compilada específicamente para el tipo de datos de sus argumentos)


In [61]:
@time sum(rand(100))


  0.057639 seconds (61.82 k allocations: 2.539 MB)
Out[61]:
50.079964423760295

In [62]:
@time sum(rand(100))


  0.000004 seconds (7 allocations: 1.063 KB)
Out[62]:
53.18026343529541

Tipos de datos elementales

Julia posee varios tipos de datos elementales, los más usados son:


In [63]:
for valor in [42, 1.0, true, 'A', "A", (true, false)]
    dump(valor) #
end


Int64 42
Float64 1.0
Bool true
Char A
ASCIIString "A"
Tuple{Bool,Bool} 

Arrays

En Julia los arrays son tipos de datos paramétricos, definidos por el tipo de datos que contienen y su dimensión. Por ejemplo una lista de Python sería un array unidimensional que contiene cualquier tipo de datos: Array{Any,1}


In [13]:
lista = [1, λ, π, "Hola mundo"]


Out[13]:
4-element Array{Any,1}:
  1                    
  0.05                 
 π = 3.1415926535897...
   "Hola mundo"        

In [14]:
typeof(lista)


Out[14]:
Array{Any,1}

Si un array contiene siempre un mismo tipo de datos, su almacenamiento en memoria es más eficiente si lo declaramos. A su vez las funciones que lo utilizan se ejecutarán de manera más eficiente/rápida (porque el compilador puede predecir el tipo de datos que va a obtener de array).


In [64]:
[62, 95, 99, 30]


Out[64]:
4-element Array{Int64,1}:
 62
 95
 99
 30

In [65]:
identidad = Float64[62, 95, 99, 30]


Out[65]:
4-element Array{Float64,1}:
 62.0
 95.0
 99.0
 30.0

In [16]:
identidad[1] # Los Arrays se acceden desde 1


Out[16]:
62.0

In [17]:
identidad[2:3] # Es posible acceder usando rangos start:end


Out[17]:
2-element Array{Float64,1}:
 95.0
 99.0

In [18]:
identidad[end] = 100 # Es posible asignar un elemento a un índice en particular, "end" permite obtener el último ítem
identidad


Out[18]:
4-element Array{Float64,1}:
  62.0
  95.0
  99.0
 100.0

Es posible indexar un array usando otro array, por ejemplo usando arrays lógicos


In [19]:
usar = identidad .> 95.0 # .> compara el array elemento a elemento


Out[19]:
4-element BitArray{1}:
 false
 false
  true
  true

In [20]:
identidad[ usar ]


Out[20]:
2-element Array{Float64,1}:
  99.0
 100.0

Existen varias dequeue functions en Julia. Dado que modifican el array que reciben, por convención sus nombres terminan en !, por ejemplo:

push! # Al final del array
pop!

shift! # Al inicio del array
unshift!

splice! # Toma un valor dentro del array

In [21]:
push!(identidad, 30)
identidad


Out[21]:
5-element Array{Float64,1}:
  62.0
  95.0
  99.0
 100.0
  30.0

Arrays multidimensionales


In [24]:
matrix = [ 0.5 0.6 
           0.7 0.8 ]


Out[24]:
2x2 Array{Float64,2}:
 0.5  0.6
 0.7  0.8

In [25]:
matrix[2,1] # Fila 2, Columna 1


Out[25]:
0.7

In [26]:
matrix[3] # La matriz se almacena de manera continua en memoria; column-major order.


Out[26]:
0.6

Dicts

Dict es a Julia lo que un hash es a Perl o un dict a Python. Una manera de almacenar datos asociados. Dict es también un tipo de dato paramétrico determinado por el tipo de sus llaves (keys) y valores (values).


In [93]:
dict = Dict('A'=>1, 'B'=>2)


Out[93]:
Dict{Char,Int64} with 2 entries:
  'B' => 2
  'A' => 1

In [94]:
dict['A'] # Accede al valor de la llave 'A'


Out[94]:
1

In [95]:
dict['C'] = 30 # Agrega un nuevo par (Pair) llave => valor al diccionario
dict['A'] = 10 # Si la llave ya existe, el valor es reemplazado
dict


Out[95]:
Dict{Char,Int64} with 3 entries:
  'B' => 2
  'C' => 30
  'A' => 10

In [96]:
dict['D'] # Error: 'D' no está en map


LoadError: KeyError: D not found
while loading In[96], in expression starting on line 1

 in getindex at dict.jl:724

In [97]:
get(dict, 'D', 0) # Es posible usar get para definir un valor default que evite el error


Out[97]:
0

In [98]:
dict


Out[98]:
Dict{Char,Int64} with 3 entries:
  'B' => 2
  'C' => 30
  'A' => 10

In [101]:
get!(dict, 'D', 0) # gest! es como get, pero agrega el valor si no se encuentra la llave
dict


Out[101]:
Dict{Char,Int64} with 4 entries:
  'D' => 0
  'B' => 2
  'C' => 30
  'A' => 10

Control de flujo

If

Evaluación condicional


In [102]:
identidad = rand() * 100.0


Out[102]:
18.600306748453544

In [103]:
if identidad == 100.0
    println("Idénticas")
elseif identidad >= 30 # Opcional
    println("Homólogas")
else  # Opcional
    println("Twilight")
end


Twilight

Loops


In [147]:
carpeta = "data"
archivos = readdir(carpeta)


Out[147]:
4-element Array{ByteString,1}:
 "benchmark.csv"         
 "iris.csv"              
 "PF09645_full.fasta"    
 "PF09645_full.stockholm"

In [148]:
i = 1
while i <= length(archivos)
    println(archivos[i])
    i += 1 # i = i + 1
end


benchmark.csv
iris.csv
PF09645_full.fasta
PF09645_full.stockholm

In [149]:
for archivo in archivos # for numero = numeros
    println(archivo)
end


benchmark.csv
iris.csv
PF09645_full.fasta
PF09645_full.stockholm

En Julia los fors son reescritos como whiles, usando las funciones start para inicializar la iteración, done para testear si se alcanzó el final de la iteración y nextpara obtener el valor de la iteración y el del próximo estado. Uno puede definir estas funciones para cualquier tipo propio que quiera hacer iterable.


In [150]:
state = start(archivos) # state = 1
while !done(archivos, state) # !( state > length(archivos) )
    (archivo, state) = next(archivos, state) # archivos[state], state + 1
    println(archivo)
end


benchmark.csv
iris.csv
PF09645_full.fasta
PF09645_full.stockholm

List comprehension como en Python


In [151]:
len = length(archivos) 
lista = Array(Int, len)

for i in 1:len
    lista[i] = filesize(joinpath(carpeta, archivos[i])) # Tamaño en bytes
end

lista


Out[151]:
4-element Array{Int64,1}:
  571
 4164
  558
 1277

In [152]:
lista = [ filesize(joinpath(carpeta, nombre)) for nombre in archivos ]


Out[152]:
4-element Array{Int64,1}:
  571
 4164
  558
 1277

Strings

Los strings son secuencias finitas de caracteres. En sus principios la bioinformática se trató del análisis de secuencias de caracteres (utilizando la codificación ASCII de 8 bits), lo que hizo popular a Perl en el área. Julia, al igual que Perl, tiene un buen soporte para strings:


In [182]:
cadena_unicode = "∃x ∈ B ∧ x ∈ A"


Out[182]:
"∃x ∈ B ∧ x ∈ A"

In [183]:
typeof(cadena_unicode)


Out[183]:
UTF8String

Es seguro iterar sobre un string (inmutable) para obtener sus caracteres. Si se quiere obtener un Vector{Char} (Array de una dimensión, mutable) se puede usar list comprehension o la función collect.


In [184]:
for char in cadena_unicode
    print(char)
end


∃x ∈ B ∧ x ∈ A

In [185]:
collect(cadena_unicode) # [ char for char in cadena_unicode ]


Out[185]:
14-element Array{Char,1}:
 '∃'
 'x'
 ' '
 '∈'
 ' '
 'B'
 ' '
 '∧'
 ' '
 'x'
 ' '
 '∈'
 ' '
 'A'

Sin embargo, acceder directamente a un string como si fuera un array no es una acción segura dado que un carácter puede estar codificado por más de un valor de 8 bits. Sólo es seguro hacer eso si la codificación es ASCII, dado que cada carácter está codificado por un sólo número entero de 8 bits. Pero no es seguro hacerlo para otras codificación. Por ejemplo, la codificación UTF-8 de ∃ (\exists<tab> en la consola) requiere de tres valores de 8 bits:


In [188]:
isascii(cadena_unicode)


Out[188]:
false

In [190]:
for i in 1:length(cadena_unicode)
    try
        println(i, " ", cadena_unicode[i])
    catch err
        println(i, " ", err) # Error al acceder cadena_unicode[i]
    end
end


1 ∃
2 UnicodeError: invalid character index
3 UnicodeError: invalid character index
4 x
5  
6 ∈
7 UnicodeError: invalid character index
8 UnicodeError: invalid character index
9  
10 B
11  
12 ∧
13 UnicodeError: invalid character index
14 UnicodeError: invalid character index

Regex: Regular Expression

Las expresiones regulares de Julia se escriben igual a las de Perl, dado que Julia utiliza la biblioteca PCRE2 (Perl Compatible Regular Expressions).


In [153]:
ext_fasta = r"\.fasta$" # r"... permite escribir una expresión regular


Out[153]:
r"\.fasta$"

In [154]:
typeof(ext_fasta)


Out[154]:
Regex

In [155]:
for nombre in archivos
    println(nombre, "\t:\t", ismatch(ext_fasta, nombre)) # ismatch es true si la regex está en el string
end


benchmark.csv	:	false
iris.csv	:	false
PF09645_full.fasta	:	true
PF09645_full.stockholm	:	false

In [156]:
ismatch(r"^>\w{4}\.\w", ">2trx.A")


Out[156]:
true

Capturando strings con expresiones regulares


In [158]:
captura = match(r"^>(\w{4})\.(\w)", ">2trx.A")


Out[158]:
RegexMatch(">2trx.A", 1="2trx", 2="A")

In [159]:
if captura != nothing
    println("PDB\t", captura[1]) # captura[1] == captura.captures[1]
    println("Cadena\t", captura[2])
else
    println("No es un PDB ID")
end


PDB	2trx
Cadena	A

In [160]:
captura = match(r"^>(\w{4})\.(\w)", ">PF00085") # nothing no imprime nada en pantalla

In [162]:
if captura != nothing
    println("PDB\t", captura[1])
    println("Cadena\t", captura[2])
else
    println("No cumple con el formato de pdb.cadena")
end


No cumple con el formato de pdb.cadena

Interpolation

La interpolación de cadenas en Julia está basada e inspirada en la interpolación de Perl. De hecho, se utiliza el mismo símbolo: \$


In [163]:
A, B = rand(1:6), rand(1:6)

"Su dado es $A, mientras el dado de IJulia es $B: $( A > B ? "usted gana" : A != B ? "IJulia gana" : "empate")"


Out[163]:
"Su dado es 4, mientras el dado de IJulia es 1: usted gana"

Lectura/Escritura de archivos

Para abrir un archivo se utiliza la función open (modos r para leer, w para escribir y a para agregar) y close para cerrarlo.


In [166]:
stream = open("data/PF09645_full.fasta", "r")


Out[166]:
IOStream(<file data/PF09645_full.fasta>)

In [167]:
for line in eachline(stream) # Itero para cada línea (incluye '\n')
    print(line) # como println pero no agrega un salto de línea
end


>C3N734_SULIY/1-95
...mp---NSYQMAEIMYKILQQKKEISLEDILAQFEISASTAYNVQRTLRMICEKHPDE
CEVQTKNRRTIFKWIKNEETTEEGQEE--QEIEKILNAQPAE-------------k....
>H2C869_9CREN/7-104
...nk--LNDVQRAKLLVKILQAKGELDVYDIMLQFEISYTRAIPIMKLTRKICEAQ-EI
CTYDEKEHKLVSLNAKKEKVEQDEEENEREEIEKILDAH----------------trreq
>Y070_ATV/2-70
qsvne-------VAQQLFSKLREKKEITAEDIIAIYNVTPSVAYAIFTVLKVMCQQHQGE
CQAIKRGRKTVI-------------------------------------------vskq.
>F112_SSV1/3-112
.....QTLNSYKMAEIMYKILEKKGELTLEDILAQFEISVPSAYNIQRALKAICERHPDE
CEVQYKNRKTTFKWIKQEQKEEQKQEQTQDNIAKIFDAQPANFEQTDQGFIKAKQ.....

In [169]:
close(stream)

open( … ) do … asegura que el archivo se cierre si ocurre algún error (implementa un try/catch)


In [170]:
open("data/PF09645_full.fasta", "r") do stream
    for line in eachline(stream)
        print(line)
    end
end


>C3N734_SULIY/1-95
...mp---NSYQMAEIMYKILQQKKEISLEDILAQFEISASTAYNVQRTLRMICEKHPDE
CEVQTKNRRTIFKWIKNEETTEEGQEE--QEIEKILNAQPAE-------------k....
>H2C869_9CREN/7-104
...nk--LNDVQRAKLLVKILQAKGELDVYDIMLQFEISYTRAIPIMKLTRKICEAQ-EI
CTYDEKEHKLVSLNAKKEKVEQDEEENEREEIEKILDAH----------------trreq
>Y070_ATV/2-70
qsvne-------VAQQLFSKLREKKEITAEDIIAIYNVTPSVAYAIFTVLKVMCQQHQGE
CQAIKRGRKTVI-------------------------------------------vskq.
>F112_SSV1/3-112
.....QTLNSYKMAEIMYKILEKKGELTLEDILAQFEISVPSAYNIQRALKAICERHPDE
CEVQYKNRKTTFKWIKQEQKEEQKQEQTQDNIAKIFDAQPANFEQTDQGFIKAKQ.....

Funciones


In [171]:
function listaralineamientos(direccion, extension::Regex=r"\.fasta$"; vacios::Bool=false)
    alns = ASCIIString[]
    for nombre in readdir(direccion)
        if ismatch(extension, nombre)
           
            if vacios || filesize(joinpath(direccion, nombre)) >0
                push!(alns, nombre)
            end
            
        end
    end   
    alns
end


Out[171]:
listaralineamientos (generic function with 2 methods)

In [174]:
methods(listaralineamientos)


Out[174]:
2 methods for generic function listaralineamientos:
  • listaralineamientos(direccion) at In[171]:2
  • listaralineamientos(direccion, extension::Regex) at In[171]:2

In [177]:
listaralineamientos("data")


Out[177]:
1-element Array{ASCIIString,1}:
 "PF09645_full.fasta"

In [173]:
listaralineamientos("data", vacios=true)


Out[173]:
2-element Array{ASCIIString,1}:
 "Empty.fasta"       
 "PF09645_full.fasta"

In [178]:
listaralineamientos("data", r"\.stockholm$")


Out[178]:
1-element Array{ASCIIString,1}:
 "PF09645_full.stockholm"

In [179]:
listarstockholm(carpeta) = listaralineamientos(carpeta, r"\.stockholm$")


Out[179]:
listarstockholm (generic function with 1 method)

In [180]:
listarstockholm("data")


Out[180]:
1-element Array{ASCIIString,1}:
 "PF09645_full.stockholm"

Documentación: doctrings


In [216]:
"""
Documentación de Σ!
-------------------

La manera más simple de agregar un **docstring** es escribirlo en un *string*, 
justo antes de la definición de la función.  

**Se puede escribir en markdown!** 😄 ( ← *emoji* )

Σ! ( *← unicode* ) tiene el **keyword argument** `valor` que por defecto vale 1.  

Simplemente suma `valor` a cada elemento del vector.
"""
function Σ!{T}(vector::Vector{T}; valor::T=one(T))
    N = length(vector)
    for i in 1:N
        vector[i] += 1
    end
    vector
end

"Σ_simd! es como Σ!, pero usando `@simd` y `@inbounds` (*SIMD*: una instrucción, múltiples datos)"
function Σ_simd!{T}(vector::Vector{T}; valor::T=one(T))
    N = length(vector)
    @inbounds @simd for i in 1:N
        vector[i] += 1
    end
    vector
end


Out[216]:
Σ_simd! (generic function with 1 method)

In [217]:
?Σ!


search: Σ! Σ_simd!

Out[217]:

Documentación de Σ!

La manera más simple de agregar un docstring es escribirlo en un string, justo antes de la definición de la función.

Se puede escribir en markdown! 😄 ( ← emoji )

Σ! ( ← unicode ) tiene el keyword argument valor que por defecto vale 1.

Simplemente suma valor a cada elemento del vector.


In [218]:
?Σ_simd!


search: Σ_simd!

Out[218]:

Σ_simd! es como Σ!, pero usando @simd y @inbounds (SIMD: una instrucción, múltiples datos)

SIMD


In [221]:
vector = Array(Int, 200_000); # Con ; no muestra/imprime la salida

In [222]:
Σ!(vector) # compilar...

@elapsed Σ!(vector)


Out[222]:
0.000258534

In [223]:
Σ_simd!(vector)

@elapsed Σ_simd!(vector)


Out[223]:
8.9887e-5

Ejecución fácil y segura de comando shell (Bash) desde Julia


In [204]:
file = joinpath("data", "benchmark.csv")


Out[204]:
"data/benchmark.csv"

In [205]:
run(`cat $file`)


Benchmark;Fortran;Julia;Python;R;Matlab;Octave;Mathematica;JavaScript;Go;LuaJIT;Java
fib;0.70;2.11;77.76;533.52;26.89;9324.35;118.53;3.36;1.86;1.71;1.21
parse_int;5.05;1.45;17.02;45.73;802.52;9581.44;15.02;6.06;1.20;5.77;3.35
quicksort;1.31;1.15;32.89;264.54;4.92;1866.01;43.23;2.70;1.29;2.03;2.60
mandel;0.81;0.79;15.32;53.16;7.58;451.81;5.13;0.66;1.11;0.67;1.35
pi_sum;1.00;1.00;21.99;9.56;1.00;299.31;1.69;1.01;1.00;1.00;1.00
rand_mat_stat;1.45;1.66;17.93;14.56;14.52;30.93;5.95;2.30;2.96;3.27;3.92
rand_mat_mul;3.48;1.02;1.14;1.57;1.12;1.12;1.30;15.07;1.42;1.16;2.36

In [212]:
comandos = pipeline(`cat $file`, `grep -v Benchmark`, `wc -l`)


Out[212]:
pipeline(pipeline(`cat data/benchmark.csv`, stdout=`grep -v Benchmark`), stdout=`wc -l`)

In [213]:
run(comandos)


7

In [214]:
readall(comandos)


Out[214]:
"7\n"

Paralelizar de manera sencilla


In [192]:
nprocs()


Out[192]:
1

In [193]:
addprocs(3)


Out[193]:
3-element Array{Int64,1}:
 2
 3
 4

In [194]:
nprocs()


Out[194]:
4

In [195]:
@everywhere const times = [1 for i=1:8] # define la variable en todos los procesos

map


In [197]:
map(sleep, times) # Cada función es compilada la primera vez que se llama
@elapsed map(sleep, times)


Out[197]:
8.009589661

In [198]:
pmap(sleep, times)  
@elapsed pmap(sleep, times)


Out[198]:
3.009619811

for & reduce


In [200]:
@time @parallel (+) for i in times
    sleep(i)
    i
end


  3.017589 seconds (4.24 k allocations: 290.034 KB)
Out[200]:
8