A day with (the) Julia (language)

Julia

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


In [1]:
superior = 1


Out[1]:
1

In [2]:
inferior = superior - 1


Out[2]:
0

In [3]:
λ = 0.05 # Unicode characters are allowed as names: \lambda<tab>

inferior <= λ <= superior


Out[3]:
true

Julia posee varios built-in types (DataTypes), como Float64 que representa a un número de coma flotante de 64 bits. Pero lo interesante es que los types creados por los usuarios son tan rápidos como los built-in.


In [4]:
typeof(λ)


Out[4]:
Float64

In [5]:
λ::Float64 # Type assertion


Out[5]:
0.05

In [6]:
λ::Int # Type assertion


LoadError: TypeError: typeassert: expected Int64, got Float64
while loading In[6], in expression starting on line 1

De hecho, Julia es un lenguaje homoiconico, por lo que su biblioteca base está escrita en Julia.
La definición de Float64 en Julia es:

abstract Number
abstract Real     <: Number
abstract AbstractFloat <: Real

bitstype 32 Float32 <: AbstractFloat
bitstype 64 Float64 <: AbstractFloat

In [7]:
super(Float64)


Out[7]:
AbstractFloat

In [8]:
subtypes(AbstractFloat)


Out[8]:
4-element Array{Any,1}:
 BigFloat
 Float16 
 Float32 
 Float64 

Para Julia es posible elegir el método más específico para cada combinación de tipos de argumentos, debido a su diseño basado en multiple dispatch. Uno puede preguntar por el método que se ejecutó usando el macro @which


In [9]:
@which λ * 100::Int


Out[9]:
*(x::Number, y::Number) at promotion.jl:168

In [10]:
@which λ * Float64(100)


Out[10]:
*(x::Float64, y::Float64) at float.jl:208

Julia es JIT (just-in-time) compiled, por lo que la primera vez que se llama a una función, su tiempo de ejecución es más lento (porque es compilada) pero la segunda vez es casi tan rápida como si corriera en C (dado que fue compilada específicamente para el tipo de datos de sus argumentos)


In [11]:
@time λ * 100


  
Out[11]:
5.0
0.000993 seconds (187 allocations: 12.339 KB)

In [12]:
@time λ * 100


  
Out[12]:
5.0
0.000002 seconds (5 allocations: 176 bytes)

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 [15]:
identidad = Float64[62, 95, 99, 30]


Out[15]:
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 [32]:
map = Dict('A'=>1, 'B'=>2, 'C'=>3)


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

In [33]:
map['D'] = 4 # Agrega un nuevo par (Pair) llave => valor al diccionario


Out[33]:
4

In [34]:
map['A'] = 5 # Si la llave ya existe, el valor es reemplazado

map


Out[34]:
Dict{Char,Int64} with 4 entries:
  'D' => 4
  'B' => 2
  'C' => 3
  'A' => 5

Accediendo valores del diccionario


In [35]:
map['B']


Out[35]:
2

In [36]:
map['E'] # Error: 'E' no está en map


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

 in getindex at dict.jl:718

In [37]:
get(map, 'E', 0) # Es posible usar get para definir un valor default que evite el error


Out[37]:
0

Control de flujo

If

Evaluación condicional


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


Out[38]:
45.44590182229684

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


Homólogas

Operador ternario

Similar al operador ternario de C (o Perl), útil para asignación condicional


In [40]:
numero = rand(1:100)


Out[40]:
34

In [41]:
if numero % 2 == 0
    es_par = "si"
else
    es_par = "no"
end

es_par


Out[41]:
"si"

In [42]:
es_par = numero % 2 == 0 ? "si" : "no"


Out[42]:
"si"

Loops


In [43]:
numero = 0
limite = 3
while numero < limite
    numero += 1
    println(numero)
end


1
2
3

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


Out[44]:
4-element Array{ByteString,1}:
 "Empty.fasta"           
 "Gaoetal2011.fasta"     
 "PF09645_full.fasta"    
 "PF09645_full.stockholm"

In [45]:
for nombre in archivos # for arch = archivos
    println(nombre)
end


Empty.fasta
Gaoetal2011.fasta
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 [46]:
state = start(archivos) # state = 1
while !done(archivos, state) # !( state > length(archivos) )
    (nombre, state) = next(archivos, state) # archivos[state], state + 1
    println(nombre)
end


Empty.fasta
Gaoetal2011.fasta
PF09645_full.fasta
PF09645_full.stockholm

List Comprehensions


In [47]:
for nombre in archivos
    println( joinpath(carpeta, nombre) )
end


data/Empty.fasta
data/Gaoetal2011.fasta
data/PF09645_full.fasta
data/PF09645_full.stockholm

In [48]:
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[48]:
4-element Array{Int64,1}:
    0
   77
  558
 1277

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


Out[49]:
4-element Array{Int64,1}:
    0
   77
  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 tiene un buen soporte para strings:


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


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

In [51]:
typeof(cadena_unicode)


Out[51]:
UTF8String

In [52]:
cadena_ascii = "A es un subconjunto de B"


Out[52]:
"A es un subconjunto de B"

In [53]:
typeof(cadena_ascii)


Out[53]:
ASCIIString

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 [54]:
for char in cadena_ascii
    print(char)
end


A es un subconjunto de B

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


∃x ∈ B ∧ x ∈ A

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


Out[56]:
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 para ASCIIStrings, 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 [57]:
for i in 1:4
    println(i, " ", cadena_ascii[i])
end


1 A
2  
3 e
4 s

In [58]:
for i in 1:4
    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

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 [59]:
ext_fasta = r"\.fasta$" # r"... permite escribir una expresión regular


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

In [60]:
typeof(ext_fasta)


Out[60]:
Regex

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


Empty.fasta	:	true
Gaoetal2011.fasta	:	true
PF09645_full.fasta	:	true
PF09645_full.stockholm	:	false

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


Out[62]:
true

In [63]:
ismatch(r"^∃x\s+", cadena_unicode) # UNICODE UTF-8


Out[63]:
true

Capturando strings con expresiones regulares


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


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

In [65]:
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 [66]:
captura = match(r"^>(\w{4})\.(\w)", ">PF00085") # nothing no imprime nada en pantalla

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


No es un PDB ID

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 [69]:
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[69]:
"Su dado es 1, mientras el dado de IJulia es 5: IJulia 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 [70]:
stream = open("data/PF09645_full.fasta", "r")


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

In [71]:
for line in eachline(stream) # Itero para cada línea (incluye '\n')
    print(line)
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 [72]:
close(stream)

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


In [73]:
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 [74]:
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[74]:
listaralineamientos (generic function with 2 methods)

In [75]:
listaralineamientos("data")


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

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


Out[76]:
3-element Array{ASCIIString,1}:
 "Empty.fasta"       
 "Gaoetal2011.fasta" 
 "PF09645_full.fasta"

In [77]:
methods(listaralineamientos)


Out[77]:
2 methods for generic function listaralineamientos:
  • listaralineamientos(direccion) at In[74]:3
  • listaralineamientos(direccion, extension::Regex) at In[74]:3

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


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

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


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

In [80]:
listarstockholm("data")


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