Coconut: Python de um jeito diferente

Funcional, mas sem exageros

O que é

coconut é uma espécie de “transpiler” que permite que você faça uso de algumas funcionalidades bem interessantes de linguagens funcionais, mas mantendo 100% de compatibilidade com o Python tradicional.

Basicamente, você escreve teu módulo/script/programa em arquivos .coco e depois “compila” cada um desses arquivos para um .py correspondente.

Prós

Um belo dialeto funcional à sua disposição

Eu não sou membro do Sagrado Culto à Função. Inclusive, apregoo por aí que funcional é overrated abertamente.

(Parece que vivemos na era do overrated, inclusive. Tudo vai mudar o mundo, agora, de alguma forma.)

Mas esse ceticismo não me impede, absolutamente, de desfrutar do que há de bom em cada “cultura”. Há uma porção de funcionalidades que eu gosto muito no paradigma funcional e admito que, usando coconut num projeto de verdade, encontrei muita coisa que facilitou minha vida ou melhorou a qualidade do código.

Pipeline e partials

Quem usa o terminal sabe o que são “pipes” e como eles tornam o fluxo de informação simples e fácil de entender.

coconut lhe dá o “operador” |ᐳ , por exemplo, que trabalha de maneira praticamente idêntica ao | do shell.

No shell:

cat file.txt | grep something

Em Python, versão tradicional:

content = cat("file.txt")
result = grep("something", content)

Em Python, versão “bem compacta”:

grep("something", cat("file.txt"))

Usando coconut:

cat("file.txt") |ᐳ grep$("something")

Nesta última linha você vê duas novidades: o |ᐳ , que pega o retorno de uma função e passa como argumento da próxima, e o $ logo após o nome da função grep , que indica que estamos lidando com uma “partial”.

Partials não são novidade no mundo do Python, e você pode encontrar helpers para isso no módulo functools da biblioteca padrão.

Mas por que um partial?

Ora, porque não queremos executar o grep imediatamente. Estamos apenas passando o primeiro argumento antecipadamente. O segundo virá pelo pipe e, aí sim, vamos executar esta função.

Ademais, fica mais fácil de entender se você pensar em termos de funções que recebem apenas um argumento.

ᐳ  7 |ᐳ factorial

Veja que absolutamente não estamos chamando factorial . Estamos apenas passando a função como uma referência ao pipe. Então, dessa mesma forma, funções que recebem parâmetros não devem ser chamadas, ainda, mas apenas referenciadas. Por isso usamos o $ no fim dos nomes: para evitar que sejam chamadas, como em:

7 |ᐳ times$(2) # -ᐳ 14

Mas não exagere!

Lembre-se que a sintaxe do Python tradicional continua disponível. Você não tem obrigação de usar pipes para tudo. Gente vindo de linguagens funcionais tem essa mania horrenda de fazer prints com pipes, por exemplo.

"Something" |ᐳ print

Simplesmente não faça isso. Não tem por que você inverter a ordem natural de leitura só porque quer parecer “coerente”. Afinal, “print something” é natural de se entender, enquanto “something print” é esquisito.

Cuidado com a função map!

coconut nos devolve a querida função map , que “mapeia” outra função sobre qualquer iterável, como em:

sum(map(pow2, values_list)) / num_values

Entretanto, lembre-se sempre que map retorna um generator que não se desenrola sozinho! Então, quando esse tipo de função acabar sendo a última de um pipeline, você precisará forçar a abertura do generator:

get_values() |ᐳ map$(pow2) |ᐳ tuple

(Repare na tuple no final. Não estou certo se esse é o melhor método, mas é simples e barato o suficiente, pelo menos.)

Match de argumentos de funções

Senti muita falta disso quando voltei da minha visita às terras do Erlang.

Geralmente, o match de argumentos é exemplificado naquele estilo tosco do universo funcional, usando a função “fatorial” como exemplo.

Desculpa, mas isso é tosco. Ninguém fica escrevendo “fatorial” no dia-a-dia. E se tem alguém que o faz, é um nicho muito pequeno.

É até engraçado (e meio triste) ver desenvolvedores encantados com linguagens funcionais, achando que passarão seus dias escrevendo “fatorial” e “média” e “desvio padrão” o tempo inteiro…

Agora, pequenas máquinas de estado, isso sim nós fazemos na vida real. Coisa que em C seria implementada usando “switch/case”, por exemplo, mas que em Python acaba nos obrigando a pré-popular um “dicionário de comandos” ou trabalhar com vários elif adjacentes.

def handle(cmd, args):
    commands = {
        "ls": do_the_ls_thing,
        "cd": do_the_cd_thing
    }

def fallthrough_function(cmd, args):
    print("Command not found:", cmd)

command_handler = commands.get(cmd, fallthrough_function)
command_handler(cmd, arguments)

Ou, quando não tem como usar um dicionário (elegantemente):

def handle(cmd, args):
    if cmd == 'ls':
        # prepare some arguments
        do_the_ls_thing()
    elif cmd == 'cd':
        # prepare some arguments
        do_the_cd_thing()
    else:
        print("Command not found:", cmd)

Já com coconut você pode trabalhar com “pattern matching” dentro do “mesmo” método:

def handle("ls", args):
    # do the "ls" thing

@addpattern(handle)
def handle("cd", args):
    # do the "cd" thing
    ...

@addpattern(handle):
def handle(cmd, args):
    ...

print("Command not found:", cmd)

O decorador addpattern pode não ser a coisa mais linda do mundo, mas handle(“ls”, args) , por exemplo, faz muito sentido, inclusive sendo fácil de ler.

Você se obriga a escrever mais funções

Mais que uma vez eu me vi “obrigado” a trocar um corpo de for inteiro por uma função por perceber que seria mais fácil alimentar o for via pipe.

#Antes:
def f():
    anchors = soup.find_all('a')
    for anchor in anchors:
    x = do_a(anchor)
    y = do_b(anchor)
    do_c(x, y)

# Depois:
def handle_anchor(anchor):
    x = do_a(anchor)
    y = do_b(anchor)
    do_c(x, y)

def f():
    soup.find_all('a') |ᐳ map$(handle_anchor)

Isso, em geral, facilita a “testabilidade” do código: você acaba com funções e métodos mais simples, que fazem menos coisas e que, portanto, tendem a ser mais fáceis de se testar.

E, novamente, você não é obrigado a fazer nada disso. Mas, quando achar que é sábio, pelo menos tem uma boa opção.

Contras

A compilação

Uma das grandes vantagens do Python é justamente não ter uma fase de compilação, que acaba sendo trazida de volta pelo coconut . Não é algo que acontece imediatamente, e se você tem um projeto realmente bem grande, isso pode se tornar bem chato.

Depuração

Os números de linhas do teu arquivo .coco não batem com os números de linhas do .py resultante. Ou seja: as mensagens de erro durante a execução apontarão para linhas “erradas”.

Olhando assim parece algo terrível, mas é algo que afeta a produtividade muito menos do que parece. Você ainda tem os nomes de funções para indicar onde ocorreu o erro e, considerando o que já foi dito acima, usando coconut você acaba tendendo a escrever mais funções e com escopos menores, então o número exato da linha acaba se tornando algo bem menos essencial, geralmente.

Um pequeno overhead

Eu não vou na onda dos “speedsters” que acham que tudo o que é rápido é bom e o que não é “rápido” (defina como preferir) é ruim. Rápido ou não-rápido são conceitos que variam muito dependendo da aplicação. E Python, no geral, é “rápido o bastante” em 100% das aplicações que tenho em mãos atualmente. Então, esse é um ponto contra muito ameno.

Usando coconut você adiciona um pequeno overhead sempre que faz uso da sintaxe que ele provê. Pode ser duas chamadas de função ao invés de uma, por exemplo, e pelo que vi, a maioria dos casos é apenas coisa desse tipo. Mas é algo a se considerar.

Eu considero esse overhead insignificante. E é bom lembrar que usar coconut não te impede de também rodar seu programa usando pypy . Ou seja: se você estiver “enxugando” seu programa ao ponto de esse overhead tornar-se significativo, provavelmente você já extrapolou o limite do que o Python pode entregar e já deveria ter mudado de linguagem.

Menor número de desenvolvedores versados

Se já não é fácil encontrar bons programadores Python, a situação fica pior se você for obrigado a procurar por programadores Python que entendam coconut .

Em um ambiente corporativo isso é um problema menor, já que qualquer um que consiga programar em Python não terá grandes dificuldades para aprender um pequeno superset da linguagem. Não chega a ser um outro mundo, na prática. Mas em projetos open source isso pode ser mais problemático, já que você conta com a boa-vontade das pessoas para te ajudarem, e agora precisa contar também com a boa-vontade das pessoas para aprenderem pelo menos um pouco de coconut .

Coisas que tive que aprender melhor

Curiosamente, mudar levemente o paradigma em um projeto de verdade acabou me obrigando a aprender melhor certas características do próprio Python “tradicional”.

local vs nonlocal vs global

Lembra que falei sobre trocar o corpo de um for por uma função? Pois é, às vezes você precisa trabalhar dentro dessa nova função com variáves “não locais”.

# Antes:
anchors = soup.find_all('a')
urls = []
for index, anchor in enumerate(anchors):
    # Things, things, things
    urls.append(href)

# Depois:
urls = []
def handle_anchor(anchor):
    nonlocal urls
    # Things, things, things
    urls.append(href)

soup.find_all('a') |ᐳ map$(handle_anchor) |ᐳ tuple

Eu admito que nunca havia precisado da palavra-chave nonlocal antes. Tanto é que batalhei um tempo tentando usar global (mesmo desconfiado de que, de fato, não fazia muito sentido).

nonlocal , basicamente, “abre o escopo” para a busca daquele nome específico (“nome” é o que as pessoas chamam de “variável”, em Python), permitindo que você use-o numa função aninhada, por exemplo, sem problemas.

Um sentido para a ordem dos parâmetros

Ao escrever funções com mais de um parâmetro, é bom já fazê-lo pensando em futuras aplicações parciais.

# Ruim:
def grep(term, content):
    ...

term |ᐳ grep$(?, content)


# Melhor:
def grep(content, term):
    ...

term |ᐳ grep$(content)

Do primeiro jeito acabamos precisando apelar para essa sintaxe do coconut que permite “pular” parâmetros ao criar um partial . Já no segundo não precisamos disso e a sintaxe fica mais clara.

E isso não foge muito do que já se faz na hora de definir a ordem dos parâmetros de uma função:

O que muda menos -ᐳ O que muda mais -ᐳ Com valor default

Como no método:

def grep(self, content, term, case_sensitive=True):
    ...

Quem muda menos? self E depois? content E depois? term E depois? As “opções”.

Context managers

Lembram (de novo) que eu falei sobre transformar o corpo de um for em uma função? Pois é. Isso também gera outras consequências interessantes. E boas.

No meu projeto eu tenho uma “barra de status” que informa o que anda acontecendo. E isso aqui era bem comum:

self.info('Doing something')
for item in collection:
    do_a()
    do_b()
    do_c()
    ...
    do_z()
    self.info() # Limpa a barra de status

Parece okay, certo? Todavia, depois de criar a função, acontecia isso aqui, por todo lado:

self.info('Doing something')
collection |ᐳ handle_item
self.info()

Feio e verborrágico.

E já é uma história bem conhecida que context managers podem ajudar a tornar seu código muito melhor. Então eu me obriguei a melhorar:

with self.info('Doing something'):
    collection |ᐳ handle_item

Esse context manager já cuida de limpar a barra de status quando “sair”.

Decorators que herdam docstrings

Isso não necessariamente está atado ao uso de coconut , mas ao criar um sistema “mágico” de “help” para alguns comandos (que eram implementados como métodos), acabei percebendo que alguns comandos “perdiam” as docstrings. Curiosamente, eram os que recebiam decoradores.

A solução?

def decorator(method):
    def new_method(self, *args, **kwargs):
        ...
        return method(self, *args, **kwargs)

    new_method.__doc__ = method.__doc__ # YAY!
    return new_method

Veio da terra do Erlang? Não conte com “actor model”

coconut faz o que pode: você não vai ganhar uma beam do Erlang assim tão fácil. Paralelismo e concorrência são tratados, na prática, como qualquer outro programa em Python trabalha. Se quiser algo melhor, terá que procurar pelas ferramentas de sempre (como “stackless Python”).

Resumo

A ideia desse artigo não era ser um “guia definitivo”, mas uma breve introdução a essa “linguagem” interessante que é a/o coconut . Eu estou usando em um projeto meu e, até agora, tenho gostado bastante. Da minha parte, espero que se popularize e melhore cada vez mais, especialmente resolvendo os “problemas” atuais.


Este artigo foi escrito originalmente em 23/10/2018.