Kodumaro :: Julia

Publicado em 17 de Dezembro, 2017
As sombras da programação.
Julia Lang

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.

Nem tudo são flores

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]

Uma nova chance

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ê.

Collatz

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

Arquivos

No próximo artigo explicarei melhor alguns detalhes da sintaxe e a relação entre função e métodos.

Funcional