Julia

De Física Computacional
Ir para navegação Ir para pesquisar

Julia é uma linguagem relativamente nova de programação que começou a ser desenvolvida em 2009 por Jeff Bezanson, Stefan Karpinski, Viral B. Shah, e Alan Edelman, um grupo do MIT. Ela foi oficialmente lançada para o mundo em 2012.

Em uma entrevista, quando perguntaram a Alan o por quê do nome Julia, ele disse que numa conversa anos atrás alguém sugeriu que Julia seria um bom nome para uma linguagem de programação.

Julia pode ser usada para propósitos gerais de programação, assim como Python, mas em sua criação foi visada a utilização de Julia para Cálculo Numérico.

Aspectos Gerais

Julia é uma linguagem de alto nível, isso significa, de maneira sucinta, que ela não é executada utilizando somente o processador, assim como acontece com linguagens de baixo nível, como Assembly, por exemplo.

É escrita em C, C++, e Scheme, usando a estrutura do compilador LLVM (para melhor entendimento, o compilador de C é o GCC, por exemplo), enquanto a maior parte da biblioteca padrão de Julia é implementada na própria Julia.

Um fato importante sobre Julia para os programadores de Python é que todas as bibliotecas de Python são usáveis através de um comando simples: PyCall.

Um fato importante sobre Julia para os programadores de C é que Julia possui APIs (Interfaces de Programação de Aplicativos) especiais para chamada de funções em C diretamente.

Velocidade

De acordo com testes feitos pelos próprios criadores de Julia, sua velocidade é similar com a de C.


Juliasvelocity.png

Fonte: [1]


Testes de Velocidade

A fim de pessoalmente testar a velocidade da linguagem de programação, foi escrito um programa em Julia de maneira similar ao programa de Python para o FTCS aplicado na Equação de Difusão (também conhecida como Equação do Calor).

A Equação da Difusão em uma dimensão pode ser escrita da forma:

No método de FTCS (Forward-Time Central-Space) definimos a derivada em t na forma não simetrizada (forward) e as derivadas em x na forma simetrizada:


E substuindo na equação da difusão, em notação discreta temos:

Observação: esse procedimento funciona bem se , segundo análise de estabilidade do método.


Dessa forma, foi escrito um programa para integrar a Equação de Difusão através do método de FTCS nas linguagens Python e Julia.

Para Python:

# FTCS aplicado na equacao da difusao. Linguagem: PYTHON

import copy
import numpy as np
import matplotlib.pyplot as plt

def init():
	L=50;D=1.;dt=0.05;dx=1.;t=0;tmax=100.
	k=D*dt/(dx*dx)    
	return L,k,dt,t,tmax

L,k,dt,t,tmax = init()

x = np.arange(0,L,1)	
f = np.zeros(x.shape)

a = int(L/3)
b = int(2*L/3)
f[a:b]=1.
f1 = copy.deepcopy(f)	
f2 = copy.deepcopy(f)

while t<tmax:
    t+=dt
    f1[1:L-1]=f[1:L-1]+k*(f[0:L-2]+f[2:L]-2*f[1:L-1])
    f1[L-1]=f[L-1]+k*(f[L-2]+f[0]-2*f[L-1])	
    f1[0]=f[0]+k*(f[1]+f[L-1]-2*f[0])		
    f=copy.deepcopy(f1)		

sum0=sum(f1)
sum1=sum(f2)	

print("Integral em t=0: %7.4f" % sum0)
print("Integral em tmax: %7.4f" % sum1)

plt.plot(x,f,x,f2)
plt.show()

Em Julia:

# FTCS aplicado na equacao da difusao. Linguagem: JULIA

using PyPlot

L=50
D=1.
dt=0.05
dx=1.
t=0
tmax=100.
k=D*dt/(dx*dx)  

x = collect(0:1:L-1)
f = zeros(L)
f1 = zeros(L)

a = trunc(Int, L/3) 
b = trunc(Int, 2*L/3)

f[a+1:b] .= 1.
f2 = deepcopy(f)

while t<tmax
	global t+=dt	
	f1[2:L-1] = f[2:L-1] + k*(f[1:L-2] + f[3:L] - 2*f[2:L-1])		
	f1[L] = f[L] + k*(f[L-1] + f[1] - 2*f[L])
	f1[1] = f[1] + k*(f[2] + f[L] - 2*f[1])	
	global f = deepcopy(f1)		
end

sum0 = sum(f1)
sum1 = sum(f2)

println("Integral em t=0: ", sum0)
println("Integral em tmax: ", sum1)

plt.plot(x,f,x,f2)
plt.show()


Foi testado o tempo de execução da parte principal dos dois programas (isto é, sem fazer o gráfico dos resultados), através do timer do terminal, como mostra na próxima figura:

Ftcs time.png

Legenda: Tempo de execução dos dois programas, em Julia e em Python, para o método de FTCS aplicado à Equação de Difusão

É possível perceber, através da figura acima, que o tempo de execução de Python para o FTCS aplicado à Equação de Difusão é menor do que o de Julia. Em razão disso, foi testado um programa simples, como mostra abaixo, que incrementa uma variável 10 vezes, sendo o seu valor inicial 0.


For.png

Legenda: Programa para incrementar o valor de uma variável, nesse caso i, 10 vezes, escrito em Julia e em Python

E posteriormente, novamente, foi testado o tempo de execução da parte principal dos programas através do terminal:

For time.png

Legenda: Tempo de execução dos dois programas, em Julia e em Python, para o programa que incrementa o valor da variável 10 vezes

É possível perceber que para esse simples programa de incrementação, Python ainda obtém melhor performance do que Julia.

Performance de Julia

Tentando entender o que traria tal performance em Cálculo Numérico para Julia, foi chegado à conclusão de que ser uma linguagem que se utilizada de variáveis dinâmicas e multi-métodos seria uma das grandes contribuições da linguagem.

Variáveis dinâmicas:

Em Python as variáveis são interpretadas em runtime, ou seja, tempo de execução => o programa checa toda hora se a variável mudou e isso torna ele mais lento

Variáveis estáticas:

Em C as variáveis são fixas, e isso é visto é em tempo de compilação => precisa pensar nas variáveis e isso torna o tempo de programação mais lento

Em Julia isso é basicamente opcional, não é preciso definir as variáveis, mas é possível, e elas podem mudar dinamicamente, em tempo de execução. E ainda, Julia se utiliza de multi-métodos para não ter que lidar com a constante checagem dos tipos das variáveis, como Python faz!

Métodos Únicos

Python => Métodos Únicos (single dispatch) - Não tenho sobrecarga de funções, eu simplesmente chamo uma função que já está escrita

Multi-métodos

Sobrecarga de funções (C++) => Multi-métodos (multiple dispatch) - Tenho uma função e dependendo dos parâmetros que eu coloco ao chamá-la, eu chamo uma função ou outra

Exemplo de sobrecarga de funções em Julia:

 
# Dependendo do tipo da minha variável x na chamada da função, será chamada uma função ou outra, mesmo que tenham o mesmo nome

function printx(x::Float64)
println(x)
end

function printx(x::String)
println(x)
end

x="Oi"

printx(x)

Performance de Julia: na prática

Para resumir, temos:

Python => dynamic (variáveis dinâmicas) single dispatch (métodos únicos)

C++ => static (variáveis estáticas) multiple dispatch (multi-métodos)

Julia => dynamic multiple dispatch (o melhor dos dois mundos!)

Na prática, é mostrado abaixo um exemplo da performance de Julia como linguagem de programação em dynamic multiple dispatch.

O mesmo programa foi escrito em Julia e em C++.

Em Julia:

abstract type Animal end

struct Cachorro <: Animal
    nome::String
end

struct Gato <: Animal
    nome::String
end

# Função de encontro que recebe tipo genérico de animal
function encontroDeAnimais(a::Animal, b::Animal)
    acao = encontro(a,b)
    println("$(a.nome) encontra $(b.nome) e $acao")
end

# Funções de expressão única que recebem diferentes tipos de animais
encontro(a::Cachorro, b::Cachorro) = "cheira" 
encontro(a::Cachorro, b::Gato) = "persegue"
encontro(a::Gato, b::Gato) = "lambe"
encontro(a::Gato, b::Cachorro) = "ignora" 

# Instanciando os bichinhos
gato1 = Gato("Luciene Maria")
gato2 = Gato("Mel")
cachorro1 = Cachorro("Batata")
cachorro2 = Cachorro("Snoopy")

# Chamando função encontroDeAnimais
encontroDeAnimais(gato1, gato2)
encontroDeAnimais(gato1, cachorro2)
encontroDeAnimais(cachorro1, cachorro2)


Em C++:

#include <iostream>
using namespace std;

class Animal { 
    public:
    string nome;
};

string encontro(Animal a, Animal b) {}

// Função de encontro que recebe tipo genérico de animal
void encontroDeAnimais(Animal a, Animal b) {
    string acao = encontro(a,b);
    cout << a.nome << " encontra " << b.nome << " e " << acao << endl;
}

class Cachorro:public Animal {};
class Gato:public Animal {};

// Funções de expressão única que recebem diferentes tipos de animais
string encontro(Cachorro a, Cachorro b) { return "cheira"; }
string encontro(Cachorro a, Gato b) { return "persegue"; }
string encontro(Gato a, Gato b) { return "lambe"; }
string encontro(Gato a, Cachorro b) { return "ignora"; }

int main() {
// Instanciando os bichinhos
Gato gato1;	gato1.nome = "Luciene Maria";
Gato gato2;	gato2.nome = "Mel";
Cachorro cachorro1;	cachorro1.nome = "Batata";
Cachorro cachorro2;	cachorro2.nome = "Snoopy";
  
// Chamando função encontroDeAnimais
encontroDeAnimais(gato1, gato2);
encontroDeAnimais(gato1, cachorro2);
encontroDeAnimais(cachorro1, cachorro2);
    
    return 0;
}

Para Julia nós obtemos o seguinte resultado:

Luciene Maria encontra Mel e lambe
Luciene Maria encontra Snoopy e ignora
Batata encontra Snoopy e cheira

Para C++ o resultado é:

Luciene Maria encontra Mel e
Falha de Segmentação

Isso acontece porque C++ não aceita que uma função utilize-se de uma variável sem antes saber qual o tipo dessa variável! Por isso, poder utilizar-se de variáveis dinâmicas quando há multi-métodos é um diferencial que acaba por incrementar a performance de Julia como linguagem de programação visada para o Cálculo Numérico.

Observação: é importante ressaltar que os testes feitos não são conclusivos para debater a performance de Julia, em termos estatísticos, e além disso, ser uma linguagem de programação que utiliza de variáveis dinâmicas e multi-métodos não é o único motivo pelo qual Julia poderia trazer tal performance em Cálculo Numérico. Podemos ainda contar com a sua capacidade de metaprogramação, ou seja, Julia pode usar macros e eles são aplicados em tempo de interpretação. Há ainda outros fatores da construção da linguagem que contribuem para sua performance que aqui não foram citados.

Aprendendo Julia

Ambiente de programação

Após feita a instalação de Julia, para abri-la no seu terminal de comando é preciso apenas digitar:

$julia

para entrar em seu ambiente de programação.

É possível, também, programar em um arquivo de texto (salvo como .jl) e executar o programa pelo terminal, fazendo simplesmente:

$julia programa.jl 

Impressão na tela

println("Hello World")

Saída:
Hello World

Variáveis

Variáveis são atribuídas dinamicamente e podem mudar.

s = 1
s = "Oi"
println(s)
println(typeof(s))

Saída:
Oi
String
s::String
s = 1
println(s)

Saída:
TypeError: in typeassert, expected String, got Int32

Condicionais

cats = 12
if cats < 1
println("You should adopt a cat.")
elseif cats >= 1 && cats <=6
println("Not enough cats.")
elseif cats >= 7 && cats <= 12
println("Still not enough cats.")
else
println("Gato have more cats.")
end

Saída:
Still not enough cats.
i = 0
while (i < 10)
global i += 1
println(i)
end

Saída:
1
2
3
4
5
6
7
8
9
10
for i = 1:10
println(i)
end

Saída:
1
2
3
4
5
6
7
8
9
10
for i in [1,2,3]
println(i)
end

Saída:
1
2
3

Arrays

# Criando um array de zeros
a1 = zeros()
println(a1)
# Posso definir o tipo e o tamanho do Array (linhas, colunas) também
a1 = zeros(Int32, 1, 4)
println(a1)

Saída:
0.0
[0 0 0 0]
# Criando um array do tipo inteiro
a2 = Array{Int32}
println(a2)
# Criando um array (de outra maneira) do tipo float
a3 = Float64[]
println(a3)

Saída:
Array{Int32,N} where N [...]
Float64[]
# Criando um array e definindo valores
a4 = [1,2,3]
#Imprimindo a4
println("Array: ", a4)

Saída:
Array: [1, 2, 3]
a4 = [1,2,3]
# Imprimindo valor de um indíce específico
println("Imprimindo primeiro elemento: ", a4[1])
# Imprimindo último valor do array
println("Imprimindo último elemento: ",a4[end])

Saída:
Imprimindo primeiro elemento: 1
Imprimindo último elemento: 3
a4 = [1,2,3]
println(a4[2])
# Valores podem mudar (diferente de Tuplas, que veremos adiante)
a4[2] = 4
println(a4[2])

Saída:
2
4
a4 = [1,2,3]
println(a4)
# Número de elementos
println("Imprimindo número de elementos: ",length(a4))

Saída:
[1, 2, 3]
Imprimindo número de elementos: 3
a4 = [1,2,3]
# Soma os valores do array
println("Imprimindo soma dos elementos: ",sum(a4))
# Põe os valores começando no índice 2
splice!(a4, 2:1, [8,9])
println("Imprimindo Array tendo feito splice!(a4, 2:1, [8,9]): ",a4)
# Remove os itens do índice 2 ao 3
4splice!(a4, 2:3)
println("Imprimindo Array tendo feito splice!(a4, 2:3): ",a4)

Saída:
Imprimindo soma dos elementos: 6
Imprimindo Array tendo feito splice!(a4, 2:1, [8,9]): [1, 8, 9, 2, 3]
Imprimindo Array tendo feito splice!(a4, 2:3): [1, 2, 3]
a4 = [1,2,3]
# Pegar valor máximo e valor mínimo
println("Imprimindo valor máximo: ",maximum(a4))
println("Imprimindo Array valor mínimo: ",minimum(a4))
# Multiplicando todos os elementos do array por outro número sem precisar de um for
println("Imprimindo multiplicação dos elementos por 2: ",a4 * 2)

Saída:
Imprimindo valor máximo: 3
Imprimindo Array valor mínimo: 1
Imprimindo multiplicação dos elementos por 2: [2, 4, 6]
# Arrays podem conter funções
a5 = [sin, cos, tan]
for n in a5
println(n(0))
end

Saída:
0.0
1.0
0.0
# Para os acostumados com range
a6 = collect(1:5)
println(a6)

Saída:
[1, 2, 3, 4, 5]

Tuplas

A maioria das funções de array funciona com tuplas.

# Valores não podem mudar
t1 = (1,2,3,4)
println(t1[2])
t1[2] = 5
println(t1[2])

Saída:
2
MethodError: no method matching setindex!(::NTuple{4,Int32}, ::Int32, ::Int32)
# Imprimindo a tupla inteira
println(t1)
# Imprimindo a tupla inteira
for i in t1
println(i)
end

Saída:
(1, 2, 3, 4)
1
2
3
4

Dicionários

A palavra-chave deve ser única.

d1 = Dict("pi"=>3.14, "e"=>2.718)
# Imprimir um valor
println(d1["pi"])

Saída:
3.14
d1 = Dict("pi"=>3.14, "e"=>2.718)
# Imprimindo todas as palavras-chaves
println(keys(d1))
# Imprimindo todos os valores
println(values(d1))
# Adicionar uma palavra-chave
d1["golden"] = 1.618
# Deleter uma palavra-chave
delete!(d1, "pi")
# Imprimindo todas as palavras-chaves
println(keys(d1))
# Imprimindo todos os valores
println(values(d1))

Saída:
["pi", "e"]
[3.14, 2.718]
["e", "golden"]
[2.718, 1.618]
d1 = Dict("pi"=>3.14, "e"=>2.718)
# Ver se a palavra-chave existe
println(haskey(d1, "pi"))
# Imprimindo palavras-chaves E valores
for kv in d1
println(kv)
end

Saída:
true
"pi" => 3.14
"e" => 2.718

Sets

Sets são arrays com elementos únicos.

st1 = Set(["Jim", "Pam", "Jim"])
# Ele não pega os elementos repetidos
println(st1)

Saída:
Set(["Pam", "Jim"])
st1 = Set(["Jim", "Pam", "Jim"])
# Adicionando um elemento
push!(st1, "Michael")
println(st1)
# Ver se um elemento está no Set
println(in("Dwight", st1))

Saída:
Set(["Pam", "Michael", "Jim"])
false
st1 = Set(["Jim", "Pam", "Jim"])
st2 = Set(["Stanley", "Meredith"])
# Combinando Sets
println(union(st1, st2))
# Imprimindo todo o elemento que os dois Sets tem em comum
println(intersect(st1, st2))
# Imprimindo elementos que estão no primeiro Set, mas não no segundo
println(setdiff(st1, st2))

Saída:
Set(["Jim", "Meredith", "Stanley", "Pam"])
Set(String[])
Set(["Pam", "Jim"])

Funções

using Printf
# Função de expressão única
soma(x,y) = x + y
x, y = 1, 2
@printf("%d + %d = %d\n", x, y, x+y)

Saída:
1 + 2 = 3
# Função de múltiplas expressões
function adoptACat(cats)
if cats <= 1
println("You should adopt a cat.")
else
println("You should adopt a cat. They're cute.")
end
end
adoptACat(2)

Saída:
You should adopt a cat. They're cute.
v1 = 5
function changeV1(v1)
v1 = 10
end
changeV1(v1)
println(v1)

Saída:
5
v1 = 5
function changeV1(v1)
v1 = 10
return v1
end
changeV1(v1)
println(v1)
v2 = changeV1(v1)
println(v2)

Saída:
5
10
# Usando variáveis globais dentro da função
v1 = 5
function changeV12()
global v1 = 10
end
changeV12()
println(v1)

Saída:
10
# Quando não sei quantos argumentos vou passar
function soma(args...)
sum = 0
for i in args
sum += i
end
println(sum)
end
soma(1,2,3)

Saída:
6
# Retornando mais de um valor
function retorna2(valor)
return (valor + 1, valor + 2)
end
println(retorna2(4))

Saída:
(5, 6)
# Map é uma função anônima que aplica uma função para cada elemento
v = map(x -> x * x, [1,2,3])
println(v)

Saída:
[1, 4, 9]

Structs

struct Estudante
name::String
saldoTRI::Float32
cartaoUFRGS::Int
end
# Criando um objeto
estudante1 = Estudante("Raquel", 10.50, 244449)
println(estudante1.name, "\n", estudante1.saldoTRI, "\n", estudante1.cartaoUFRGS)

Saída:
Raquel
10.5
244449

Tipos Abstratos

Tipos abstratos não podem ser instanciados como as Structs (instanciar => fazer estudante1 = Estudante(“Raquel”, 10.50, 244449) => criar um objeto) MAS podem ter subTipos e isso é muito útil!

abstract type animal end
struct cachorro <: animal
nome::String
latido::String
end
struct gato <: animal
nome::String
miado::String
end
cachorro1 = cachorro("Urso", "Au Au")
gato1 = gato("Lucia Irene", "Miau")
# Criando funções pros subTipos
function fazerSom(animal::cachorro)
println("$(animal.nome) diz $(animal.latido)")
end
function fazerSom(animal::gato)
println("$(animal.nome) diz $(animal.miado)")
end
fazerSom(cachorro1)
fazerSom(gato1)

Saída: 
Urso diz Au Au
Lucia Irene diz Miau