4 pontos por GN⁺ 2025-08-20 | 1 comentários | Compartilhar no WhatsApp
  • Programar da esquerda para a direita significa que, assim que o código é digitado, o programa permanece em um estado válido, maximizando o suporte das ferramentas, como o autocompletar do editor
  • As list comprehensions do Python atrapalham o autocompletar por causa de variáveis não declaradas e da ausência de inferência de tipos
  • Rust e JavaScript permitem compor o programa naturalmente da esquerda para a direita, tornando mais intuitivos o uso de variáveis e a descoberta de métodos
  • O estilo funcional em C e Python prejudica uma experiência de codificação eficiente por causa da baixa descobribilidade dos nomes de funções ou da estrutura
  • Em lógicas mais complexas, o código desenvolvido da esquerda para a direita é mais fácil de ler e oferece melhor manutenção e extensibilidade

Programar da esquerda para a direita

O código precisa ser válido no momento em que é digitado


Limitações das list comprehensions em Python

  • A sintaxe de list comprehension do Python words_on_lines = [line.split() for line in text.splitlines()] exige acessar uma variável não declarada (line), o que faz com que o editor não consiga oferecer autocompletar nem inferência de tipos de forma adequada
  • Durante o processo de digitação parcial do código
    • ao digitar algo como words_on_lines = [line.sp, o editor não sabe o tipo de line e não consegue sugerir métodos
    • também fica mais difícil detectar erros em potencial, como um typo no nome da variável (lime, por exemplo)
  • Para receber sugestões corretas, é preciso escrever código incompleto, o que torna o processo pouco intuitivo e incômodo

Composição da esquerda para a direita em Rust

  • No exemplo em Rust (let words_on_lines = text.lines().map(|line| line.split_whitespace());),
    • no momento em que a variável (line) aparece pela primeira vez junto com a declaração da função anônima, ela já é tratada como declarada, permitindo autocompletar e sugestão de métodos imediatamente
    • na prática, até o método split_whitespace pôde ser encontrado facilmente graças às sugestões automáticas
  • Como o programa permanece sempre em um estado parcialmente válido, a IDE ou o editor consegue oferecer suporte à codificação em tempo real

Divulgação progressiva (Progressive Disclosure) e usabilidade de APIs

  • Divulgação progressiva (Progressive Disclosure) é um princípio de design em que o usuário só encontra a complexidade na medida em que precisa dela, e isso também pode ser aplicado à programação
    • Ex.: semelhante à UX de um processador de texto em que opções relacionadas só aparecem ao adicionar uma imagem
  • A linguagem C oferece pouco desse suporte
    • como nem todas as funções relacionadas a FILE *file podem ser exploradas via file., é preciso decorar padrões de nomes de função (fread, fclose etc.), e fica difícil descobrir funcionalidades
    • já em uma linguagem ideal, as sugestões de métodos via file. permitiriam descobrir progressivamente os recursos relacionados com facilidade

Diferença na descobribilidade de funções e métodos

  • Comparação entre os exemplos map(len, text.split()) em Python e text.split(" ").map(word => word.length) em JavaScript
    • em Python, como nomes de função como len, length ou size não são previsíveis, é preciso tentar várias opções para descobrir qual realmente funciona
    • em JavaScript, basta digitar .l depois de word. para o editor sugerir métodos como length, o que dá alta descobribilidade
    • até funções de ordem superior como map deixam claros de imediato o valor retornado e o tipo de dado

Quanto mais complexa a lógica, maior a vantagem da escrita estruturada

  • Em lógicas mais complexas (código longo em Python com filter e lambda aninhados),
    • é preciso verificar repetidamente o início e o fim do código, e surgem problemas de legibilidade reduzida e dificuldade de compreensão em condições e pareamento de parênteses
  • Na versão equivalente em JavaScript, é possível ler e entender o código sequencialmente, de cima para baixo e da esquerda para a direita

Princípio central

O código precisa ser válido em cada momento da digitação

  • Mesmo ao digitar apenas text, o programa continua em um estado válido
  • Mesmo após escrever text.split(" "), e depois continuar com .map(word => word.length), o estado intermediário do programa permanece sempre válido
  • Esse padrão de codificação aumenta a possibilidade de suporte em tempo real pelo editor e, em um ambiente REPL, também permite verificar os resultados imediatamente

Conclusão

  • O design de APIs e linguagens deve dar suporte para que o código possa ser digitado naturalmente da esquerda para a direita, formando um programa válido em cada etapa intermediária
  • Um bom design de API é a chave para melhorar essa experiência de codificação

1 comentários

 
GN⁺ 2025-08-20
Comentários do Hacker News
  • Um dos problemas do SQL é que a consulta começa com SELECT em vez de FROM, então fica difícil entender de imediato com qual entidade (tabela) você está lidando, e isso também atrapalha editores inteligentes a ajudarem de forma mais eficiente na escrita de consultas. Ir de FROM -> SELECT -> WHERE parece mais natural, especialmente porque você define os nomes das colunas na cláusula SELECT e depois os referencia no WHERE. Na verdade, acho que seria possível omitir a cláusula SELECT em vez de SELECT * FROM table e simplesmente escrever FROM table. Talvez eu soe como um velho rabugento por reclamar disso, mas é só uma saudade pessoal minha
    • PSQL e PRQL de fato usam uma ordem de consulta com FROM primeiro. O BigQuery também adicionou recentemente uma sintaxe de pipe/seta, e há extensões da comunidade para o DuckDB, então recomendo: DuckDB - PSQL, DuckDB - PRQL
    • O motivo de o SQL ser escrito assim é que, na base da álgebra relacional, a projeção (Projection) sempre vem primeiro. Por isso, pelo padrão, não se pode usar alias de coluna no WHERE, porque a seleção (WHERE) acontece antes da projeção (SELECT). Como referência, no MySQL 8 também existe a sintaxe TABLE <table>
    • Na prática, a ordem de processamento interno da maioria dos motores SQL é FROM -> WHERE -> SELECT. Por isso, aliases de coluna definidos no SELECT podem ser usados em GROUP BY, HAVING e ORDER BY, mas não no WHERE
    • Em C#, DSLs que compilam para SQL (LINQ-to-SQL) também têm uma estrutura em que FROM vem primeiro. E gosto dessa estrutura porque, ao escrever outras cláusulas na IDE, já dá para receber sugestões de campos imediatamente graças ao autocompletar
    • A linguagem de consulta para análise de dados do Azure, Kusto, também usa um formato parecido com pipe Introdução a consultas Kusto, assim como o estilo LINQ do .NET. Sinceramente, acho que o SQL deveria adotar de forma mais ativa variações que começam com FROM, e nem me parece algo difícil. Falta tentativa de melhorar a usabilidade
  • Acho difícil entender por que Python é tão amado. Quando mais de duas pessoas trabalham juntas, a linguagem se torna infinitamente dolorosa. O ponto levantado pelo autor é só a ponta do iceberg
    • Acho que é parecido com o motivo de as pessoas não migrarem em massa para linguagens da família Lisp. Rigor matemático não significa necessariamente legibilidade. As comprehensions de list/dict/set do Python são como loops for com tipo definido. Todo mundo se preocupa com a tipagem frouxa do Python, mas é estranho que a única construção que deixa claro o tipo de retorno (list comprehension) vire alvo. Na maioria das outras linguagens, incluindo Rust, a ordem também não é "from iter as var". Também é divertido comparar a sintaxe de chamada de função entre linguagens (assim como existe functools.map no Python)
    • Só porque você não entende alguma coisa não quer dizer que isso seja uma virtude. Claramente existe algum motivo para Python ser tão amado. Claro que ele tem desvantagens, mas isso por si só não significa muito. É preciso comparar o conjunto de prós e contras, e fazer isso também em relação a outras linguagens
    • Eu também gosto de Python (desde que seja para times pequenos e programas curtos, com vida útil curta). A falta de tipagem estática acelera a implementação, mas um sistema de tipos forte evita que tudo desmorone por completo. Acho que isso ajuda a explicar a popularidade dele em ciência de dados; ele é muito conveniente para exploração. Por outro lado, há desvantagens claras em programas grandes ou mantidos por várias equipes no longo prazo. No fim, não existe linguagem universal, e pelo menos precisamos de uma "linguagem para tentar e avançar rápido (Soft)" e outra "linguagem boa para manter por muito tempo (Hard)"
    • Eu também concordava totalmente com essa opinião no passado, mas anotações e checagem de tipos tornaram muito mais fácil colaborar com código Python escrito por outras pessoas. Ainda acho que ele não vai bem até projetos muito grandes, mas, com tipos, Python virou minha linguagem de script favorita
    • Eu também evito coisas como list comprehensions em codebases compartilhados e prefiro um estilo de Python bem simples. É conhecida como uma linguagem em que "deve haver apenas uma maneira", mas na prática coexistem maneiras demais. Pessoalmente, list comprehensions são divertidas e satisfatórias, mas, se todo mundo tiver que seguir um único caminho, acho que essa sintaxe não deveria existir
  • Concordo com a ideia de que "o programa deve ser válido no momento em que você o digita", mas, na prática, nem sempre escrevemos código de forma sequencial, da esquerda para a direita, linha por linha. Muitas vezes escrevemos primeiro outra parte no meio ou deixamos a declaração de uma variável para depois. Às vezes até usamos uma variável e só a declaramos muito depois
    • Como o código é escrito uma vez e lido dezenas ou centenas de vezes, acho muito mais fácil ler código que pode ser percorrido sequencialmente do que código que exige saltos
    • Na verdade, essa discussão foge um pouco do ponto central do texto, mas é uma perspectiva interessante
    • Concordo completamente. Só escrevo código em ordem desde o começo quando crio um arquivo novo. Ao adicionar um campo, não começo necessariamente pela definição da classe; vou direto ao código que usa esse campo. Ao melhorar uma condicional, também é comum passar temporariamente por um estado inválido (com erro)
    • Também concordo com isso, mas um princípio importante relacionado é que uma estrutura que "nem deixa você compilar porque você ainda não terminou de programar" é exagerada demais. Erros deveriam ser não bloqueantes, mas algumas linguagens barram código incompleto por completo (por exemplo, variável não usada, return ausente etc.)
    • Às vezes me incomoda sentir que a IDE não entende muito bem a ordem em que eu realmente escrevo o código
  • Em algumas IDEs, há um recurso de templates de código em que você digita uma abreviação e ela se expande para uma estrutura de código, preenchendo cada placeholder com a tecla Tab. Nesses casos, a ordem de navegação por Tab não precisa necessariamente ser da esquerda para a direita, então algo como {3} for {2} in {1} também é possível. Essas ferramentas oferecem um meio-termo entre "sintaxe boa de ler" e "sintaxe fácil de digitar". Mesmo recorrendo a tooling, eu tendo a priorizar uma sintaxe boa de ler. Não acho que exista motivo para insistir exclusivamente na estrutura "for-in"
  • Hoje em dia, o consenso no Hacker News parece ser que o Python errou ao não incluir um operador pipe. Quando migrei de Mathematica para R, percebi rapidamente o valor do pipe. Ao escrever transformações de dados passo a passo em ciência de dados, ele é realmente intuitivo e fácil de ler. Como Python é usado em várias áreas, fico curioso se o pipe também traria vantagens fora do contexto de análise de dados. Tento entender por que Python não o adotou
    • Indo um passo além do operador pipe, acho que reverse assignment também valeria a pena. Em vez de atribuir o resultado a uma variável como em 'let foo = ...', eu gostaria de experimentar algo como '... =: foo'
    • O operador pipe do R (especialmente no tidyverse R) é o maior "killer app" para mim. Não conheço outra linguagem em que trabalhar com dados seja tão fácil e prazeroso. Por exemplo, em vez de aninhar uma receita de cookies como bake(divide(add(knead(mix(flour, water, sugar, butter)),eggs),12),450,12), usar pipe em mix(flour, water, sugar, butter) %>% knead() %>% add(eggs) %>% divide(12) %>% bake(temp=450, minutes=12) é muito mais fácil e agradável de ver
    • No pandas do Python, a sintaxe com pipe fica assim:
      result = (df
       .pipe(fun1, arg1=1)
       .pipe(fun2, arg2=2)
      )
      
      Em R:
      result <- df |>
       fun1(., arg1=1) |>
       fun2(., arg2=2)
      
      Ambos são legíveis, mas a vantagem do R é que o pipe funciona melhor também fora de dataframes
  • Essa discussão é quase uma guerra religiosa, como o debate entre linguagens FP (funcionais) vs OOP (orientadas a objetos), ou como a rivalidade entre vim e emacs. No vim, o operador vem primeiro; no emacs, a ordem da seleção vem primeiro. Linguagens que "soam como inglês" normalmente têm estrutura com verbo primeiro (como Lisp/Scheme), enquanto idiomas como alemão ou tâmil, em que o verbo vem no fim, combinam mais com o estilo OOP (substantivo primeiro). Por exemplo, em tâmil seria algo como "water drink", enquanto em inglês é "drink water". Por isso, pode haver gente que ache o vim mais confortável. Em vez de um estilo ser melhor que o outro, eles também podem refletir a ferramenta ou a preferência de cada pessoa, e hoje em dia, com modelos de linguagem, dá para fazer praticamente qualquer coisa
    • Sobre a ideia de que, "se for projetado para soar como inglês, então o verbo vem primeiro", isso vale para linguagens imperativas, mas em linguagens declarativas, se for para soar como inglês, o sujeito vem primeiro
    • Sobre a ideia de que "o alemão sempre coloca o verbo no fim", na verdade, em frases simples o verbo fica na segunda posição ("I drink water" → "Ich trinke Wasser"), então não é que ele vá sempre para o final da frase
    • Sobre dizer que no vim o operador vem primeiro, na verdade o Kakoune funciona ao contrário, e acho esse jeito muito mais lógico Explicação sobre o Kakoune
  • Por outro lado, a sintaxe do Python from some_library import child_module é muito intuitiva. Em JS, uma estrutura como import { asYetUnknownModule } from SomeLibrary parece bem menos intuitiva
    • Em JS, se você usar namespace import assim:
      import * as someLibrary from "some-library"
      someLibrary.someFunction()
      
      na prática o autocompletar da IDE funciona muito bem, o que considero positivo Explicação da MDN sobre namespace import
    • Acho estranho esse apego à palavra-chave "from". Poderia simplesmente ser:
      import SomeLibrary {
        asYetUnknownModule
      }
      
  • O ReScript mudou sua API de data-last para data-first exatamente por esse motivo. Graças à excelente inferência de tipos, quase sempre há autocompletar correto e compatível com os tipos, o que torna a experiência de desenvolvimento muito boa. Claro que, se você declarar uma função sem referência a uso (então não há como saber o tipo), o problema continua, mas isso se resolve adicionando tipos ou chamando a função antes. Também recomendo este post: Comparação entre data-first e data-last
  • Venho defendendo essa visão há bastante tempo, e ela se conecta com o motivo de Ruby sempre ter parecido muito mais fácil para mim. Eu, em particular, nunca usei nem Python nem Ruby profundamente em produção, mas não entendo bem por que Python passou a vir tão instalado e tão usado. Ruby também não é perfeito, mas não parece que muita gente que escreve scripts precise lidar com as mudanças complexas que Python tem. Pelo menos Ruby não teve, nos últimos 10 anos, um grande episódio de conflito entre versões
  • No geral, concordo totalmente com os pontos levantados no artigo. Acho que uma estrutura em que o contexto vem primeiro e a leitura flui da esquerda para a direita combina melhor com LLMs e autocompletar. Ainda assim, em vez de escrever o código de exemplo como len(list(filter(lambda line: all([abs(x) >= 1 and abs(x) <= 3 for x in line]) and (all([x > 0 for x in line]) or all([x < 0 for x in line])), diffs))), seria muito melhor usar um array NumPy, já que assim não é preciso criar novas listas na memória e também fica bem melhor para tratar a linha inteira de uma vez. Por exemplo:
    sum(1 for line in diffs
      if ((np.abs(line) >= 1) & (np.abs(line) <= 3)).all()
        and ((line > 0).all() or (line < 0).all()))
    
    Isso reflete muito melhor a ideia de "da esquerda para a direita"
    • A versão com numpy ainda é um pouco enigmática ("line > 0" tudo bem, mas as regras de broadcasting podem ficar complexas), e APIs de coleção de linguagens com tipos mais rígidos, como o exemplo em JavaScript do autor ou C#, Java e Scala, são mais limpas. Minha preferência é Kotlin, porque dá para escrever assim:
      diffs.countIf { line -> 
        line.all { abs(it) in 1..3 } and ( 
          line.all { it > 0} or
          line.all { it < 0}
        )
      }