Há uns sete ou oito anos um amigo meu me recomendou dar uma olhadinha numa linguagem de programação nova que estaria sendo amplamente usada em machine learning e seria uma “Python melhorada”. Essa linguagem era Julia.
Na época olhei e não achei nada de mais. Este ano resolvi dar outra olhada e me supreendi com o que aprendi.
Julia é linguagem de programação funcional impura, como Standard-ML, OCaml e F♯, mas com suporte ao paradigma imperativa, de sintaxe fortemente inspirada em Python, com foco em análise numérica e computação científica, e otimizada para paralelismo e computação distribuída. Foi descrita como tendo a elegância de Python e a performance de C.
Só por essa introdução, já dá pra ver que não é possível abordar a linguagem profundamente em alguns poucos artigos, mas posso adiantar que há um certo exagero nos elogios a Julia.
Pra começar, a performance de Julia não é nada excepcional: ela faz compilação JIT, mas o desempenho deixa a desejar mesmo entre plataformas JIT. Seu desempenho é bastante impressionante se comparado a plataformas interpretadas.
Quanto à elegância, seria um ponto a favor da linguagem, com suas estruturas funcionais realmente elegantes, porém a linguagem só demonstra eficiência se o código for escrito de modo quase procedimental, anulando essa vantagem. Além disso, não é difícil conseguir uma falha de segmentação ou um core dump.
Outra coisa que demonstra certo amadorismo é a postura dos desenvolvedores. Darei um exemplo.
Há algumas semanas quando saiu a versão 0.6.0, muitos códigos que funcionavam perfeitamente na versão 0.5 simplesmente pararam de funcionar, e a linguagem não se comportava como a documentação dizia que deveria. Procurando em fóruns na Web, percebi que não era um problema exclusivo meu, e que toda a comunidade reclamava das mesmas coisas. Diante do problema, a solução dos desenvolvedores foi tirar a documentação do ar.
Foi um banho de água fria e o suficiente para me fazer desistir da plataforma.
Mais recentemente, quando lançaram a versão 0.6.1 e voltaram com a documentação, resolvi tentar de novo e descobri que meus códigos antigos voltaram a funcionar com pouquíssimas alterações.
[update 2017-12-18]
No dia seguinte à publicação deste artigo, foi lançada a versão 0.6.2.
[/update]
Voltei então a experimentar Julia. Apesar das frustrações causadas pelos elogios exagerados e falta de profissionalismo da equipe envolvida, a plataforma é realmente boa. A sintaxe é sim elegante, a performance razoável e o ecossistema interessante.
A instalação de pacotes é muito simplificada. Por exemplo, para instalar o pacote de suporte RESTful o comando é:
sh> julia -e 'Pkg.add("Resftful")'
E Julia faz todo o resto pra você.
Como um exemplo de um módulo Julia, vamos implementar a conjectura de Collatz. Não veremos paralelismo, mas veremos criação de módulo, interface de reiteradores, struct e testes unitários.
Podemos começar com o arquivo Collatz.jl
, que definirá o móduloCollatz
. O arquivo pode começar assim:
module Collatz
#=
= O código vai aqui.
=#
end
Para representar o reiterador (e os passos da reiteração) usaremos um struct imutável que embrulha um inteiro sem sinal. Criaremos dois construtores para fazer a conversão de número para o reiterador:
struct Iter
value::UInt
Iter(value::UInt) = (value ≡ zero(value)) ?
throw(InexactError()) : new(value)
Iter(value::Integer) = value |> UInt |> Iter
end
O primeiro construtor recebe um inteiro sem sinal, se for igual a zero, levanta um erro do tipo InexactError
, se não repassa para o
construtor padrão, que envelopará o inteiro. O segundo construtor recebe
um inteiro qualquer, converte para sem sinal e repassa para o construtor
cabível (o anterior).
Para expor nosso reiterador, vamos criar um método que retorne o reiterador. No cabeçalho do arquivo, dentro do módulo, exporte:
export collatz
E após a definição do struct:
collatz(value::Integer) = Iter(value)
Isso diz que a função collatz
, quando recebe um parâmetro inteiro, retorna um reiterador embrulhando o valor.
Podemos escrever um teste: crie o arquivo test.jl
:
#!/usr/bin/env julia
include("Collatz.jl")
module CollatzTest
import Base.Test
import Collatz
using Collatz: collatz
@testset "Collatz.Iter" begin
@testset "should return iterator" begin
iter = @inferred collatz(4)
@test isa(iter, Collatz.Iter)
@test iter.value == 4
end
end
end
Já pode rodar o teste:
sh> chmod +x test.jl
sh> ./test.jl
Test Summary: | Pass Total
Collatz.Iter | 1 1
should return iterator | 1 1
O macro @testset
define um grupo de testes, @test
define um teste e @inferred
forçar a inferência de tipo para permitir
testes com isa
.
Podemos testar também os casos de erro:
@testset "collatz tests" begin
@testset "should fail on zero" begin
@test_throws InexactError collatz(0)
end
@testset "should fail on negative integer" begin
@test_throws InexactError collatz(-1)
end
@testset "should fail on non integer" begin
@test_throws MethodError collatz(1.0)
end
end
Voltando ao módulo Collatz
, para permitir a reiteração, precisamos importar algumas funções e criar métodos para elas:
import Base.SizeUnknown,
Base.done,
Base.iteratorsize,
Base.next,
Base.start
Essas funções são usadas internamente pelo data model de Julia para gerenciar reiteração.
Bem, nesse tipo de sequência não se sabe o tamanho da reiteração, portanto precisamos definir um método para iteratorsize
que retorne tamanho
desconhecido:
iteratorsize(::Iter) = SizeUnknown()
E podemos começar a reiteração com nenhum valor:
start(::Iter) = nothing
Passo seguinte é o próprio valor envolvido pelo reiterador:
next(iter::Iter, ::Void) = (iter.value, iter.value)
Os passos seguintes serão definidos pelo método nextstep
:
next(::Iter, state::UInt) = state |> nextstep |> v -> (v, v)
Enquanto nextstep
implementa o cálculo do passo da conjectura:
nextstep(value::UInt) = (value % 2) ≡ zero(value) ? value ÷ 2 : 3value + 1
Para a parada, se nada foi retornado ainda, não terminou:
done(::Iter, ::Void) = false
E termina quando chega a um:
done(::Iter, state::UInt) = state ≡ one(state)
Resta apenas testar. Na suite de testes collatz tests
acrescente o seguinte teste:
@testset "should be iterable" begin
@test [n for n in collatz(5)] == [5, 16, 8, 4, 2, 1]
end
Temos aqui uma list comprehension para transformar a sequência em uma lista testável. Poderíamos fazer diferente:
@testset "should be iterable" begin
@test collect(collatz(5)) == [5, 16, 8, 4, 2, 1]
end
No próximo artigo explicarei melhor alguns detalhes da sintaxe e a relação entre função e métodos.