1 pontos por GN⁺ 2023-11-16 | 1 comentários | Compartilhar no WhatsApp
  • O ponto central são duas regras de estruturação de código: puxar as ramificações condicionais para o lado do chamador e empurrar o processamento repetitivo para o lado das operações em lote
  • Ao subir o if, fica mais fácil impor pré-condições com tipos ou assert, e o fluxo de controle complexo fica reunido em um só lugar, facilitando encontrar condições duplicadas e ramificações mortas
  • Uma estrutura que cria um enum e logo depois faz match nele novamente repete a mesma condição como ramificação, estrutura de dados e nova ramificação, então pode haver espaço para simplificar para chamadas foo(x) e bar(y)
  • Ao descer o for, operações em lote se tornam a unidade básica, em vez de iteração sobre objetos individuais, permitindo amortizar custos de inicialização, mudar a ordem de processamento, vetorizar e otimizar com struct-of-array
  • Tirar a condição de dentro do loop reduz ramificações no hot loop e também leva a estruturas como a do TigerBeetle, que amortizam o custo de tomada de decisão do plano de controle por meio do processamento em lote no plano de dados

Subir o if

  • Uma condição if dentro de uma função deve primeiro ser avaliada para ver se pode ser movida para o chamador
    • O exemplo considera que uma função que recebe Walrus diretamente é uma forma melhor do que uma que recebe Option<Walrus> e retorna quando é None
    • Em vez de verificar uma pré-condição dentro da função e “não fazer nada”, é mais claro o chamador verificar e impor sua validade por meio de tipos ou assert
  • A refatoração de subir verificações de pré-condição pode se propagar ao longo do caminho de chamadas e, como resultado, reduzir o número de verificações no sistema como um todo
  • Fluxo de controle e if aumentam a complexidade e tendem a ser fonte de bugs
    • Ao subir o if, a lógica complexa de ramificação fica concentrada em uma função, enquanto o trabalho real desce para sub-rotinas lineares
    • É mais fácil encontrar duplicações e condições mortas quando um fluxo de controle complexo está em uma única função visível na tela do que espalhado por todo o arquivo
  • A refatoração “dissolving enum” é uma forma de eliminar o padrão em que uma ramificação é transformada em estrutura de dados e depois ramificada de novo
    • Se f() cria E::Foo(x) ou E::Bar(y) conforme uma condição, e g(e) depois usa match para chamar foo(x) ou bar(y), a mesma condição se repete em várias formas
    • Ao puxar a condição para main(), a estrutura se simplifica para if condition { foo(x) } else { bar(y) }

Descer o for

  • Do ponto de vista orientado a dados, programas e caminhos quentes geralmente lidam com muitos objetos, então introduz-se um lote de objetos e toma-se a operação em lote como caso padrão
    • A versão escalar para objeto individual se torna um caso especial da operação em lote
    • frobnicate_batch(walruses) é uma forma melhor do que for walrus in walruses { frobnicate(walrus) }
  • O principal benefício é desempenho
    • Ao lidar com o lote inteiro de uma vez, é possível amortizar o custo de inicialização
    • Também é possível mudar a ordem de processamento com flexibilidade e adotar vetorização ou uma abordagem struct-of-array, processando primeiro um campo de todas as entidades
    • Como exemplo extremo, há o Vectorized Interpreters Talk
  • Um exemplo interessante é a multiplicação de polinômios baseada em FFT
    • Avaliar um polinômio simultaneamente em vários pontos pode ser mais rápido do que realizar várias avaliações em pontos individuais
  • As regras de if e for podem ser aplicadas juntas
    • A boa forma é uma estrutura em que for walrus in walruses é executado por ramificação, fora de if condition
    • A má forma é uma estrutura que avalia if condition a cada vez dentro do loop for
    • Isso evita reavaliar a condição, remove ramificações do hot loop e pode viabilizar vetorização
  • Esse padrão se aplica tanto no nível micro quanto no macro
    • A arquitetura do TigerBeetle processa lotes de objetos simultaneamente no plano de dados, amortizando o custo das decisões do plano de controle
    • Embora a principal motivação do conselho sobre for seja desempenho, operar no nível de coleções, como no jQuery, também pode ajudar na expressividade

1 comentários

 
GN⁺ 2023-11-16
Opiniões no Hacker News
  • No começo, achei inesperado que este texto estivesse recebendo tanta resistência, mas, ao lembrar que isso é um conselho de design orientado a dados, ficou compreensível
    Como muitas pessoas neste fórum, eu também passo a maior parte do tempo criando apps web corporativos e, nesse contexto, esse conselho pode parecer besteira
    Se no trabalho do dia a dia você não precisa se preocupar com o cache de instruções, parece correta a heurística de que, em geral, dá para ignorar esse conselho
    Se quiser ter uma noção de quando esse conselho se torna importante, vale procurar “Typical C++ Bullshit”, de Mike Acton; este texto parece uma organização mais fácil de entender daquele conteúdo
    Eu simpatizo bastante com as preocupações de Casey Muratori, mas a maior parte do software corporativo precisa otimizar mais para modificabilidade e correção do que para desempenho, isto é, para “programação ao longo do tempo”

    • Acho que esse conselho ainda é bastante aplicável à programação orientada a objetos
      Desenvolvedores tentam dividir lógicas de negócio complexas em pequenos métodos private dentro de classes para deixá-las DRY, e “suba o if” ajuda a impedir que a lógica de ramificação fique espalhada por vários métodos
      “Desça o for” também é importante. Muitos fluxos de chamada acabam terminando em consultas caras ao banco de dados, e é comum que um loop em nível superior provoque várias chamadas ao DB em níveis inferiores
      Muitas vezes essa repetição pode ser substituída por uma cláusula where ou um join em SQL, e também é melhor empurrar agregações e filtragens para perto do DAO, em vez de buscar um monte de objetos e iterar sobre eles, pois assim fica mais fácil otimizar perto do DB
      Porém, como todo princípio de design, não se deve aplicá-lo como dogma; é preciso julgar conscientemente
    • Design orientado a dados parece sempre provocar as pessoas. Provavelmente porque dá a entender que muitas partes da abordagem dominante atual, orientada a objetos, estão erradas
      É uma pena ver modificabilidade e desempenho como coisas opostas. Na prática, ainda não vi evidências convincentes de que os dois realmente entrem em conflito
    • Modificabilidade, correção e desempenho não são mutuamente exclusivos; pelo contrário, frequentemente andam juntos
      Um dos aspectos mais importantes do desempenho é tornar as coisas pequenas. Código pequeno, estruturas de dados pequenas e um número menor de instruções executadas são essenciais, e “pensar no cache de instruções” é, no fundo, escrever código pequeno
      Quanto menor o código, menos espaço há para bugs entrarem, mais fácil é entendê-lo de uma vez só e melhor é garantir cobertura de testes, o que favorece a correção. A modificabilidade também melhora, porque, quanto menor o código, menores são as mudanças
      É claro que existem otimizações que tornam o código mais complexo, como paralelização, caching e otimizações de baixo nível, mas isso é apenas parte da otimização de desempenho; um programador que leva desempenho a sério não faz esse tipo de coisa sem profiling e análise
      Mesmo que if/for e for/if não sejam muito diferentes funcionalmente, um deles pode ser mais rápido; então, se é possível usar a forma correta sem custo, não há motivo para escolher deliberadamente a mais lenta
    • Ouvindo John Ousterhout, na prática modificabilidade e correção não entram tanto em conflito com desempenho quanto se pensa à primeira vista
      Programas simples tendem a ter uma pilha de chamadas curta e a evitar abstrações complexas e caras, portanto usam menos memória e rodam mais rápido
      Ao buscar desempenho de nível máximo, o programa pode ficar complexo, mas a verdadeira simplicidade geralmente traz desempenho razoável
      “Descer o for” é principalmente um conselho orientado a dados, mas “subir o if” está mais próximo de tornar o programa mais simples. Mais precisamente, trata-se de aumentar a localidade do código-fonte, e o ponto central é reunir a lógica de ramificação em um só lugar: https://loup-vaillant.fr/articles/source-of-readability
    • Acho que os dois conselhos também se aplicam bastante a apps corporativos
      Especialmente “desça os loops” é poderoso em apps CRUD. Processar criações e atualizações em lote sempre que possível frequentemente economiza um tempo muito maior do que gargalos de CPU
      A diferença entre items.map(insertToDb/postToServer) e insertToDb/postToServer(items) quase sempre é de várias ordens de grandeza
      Já vi esse tipo de otimização reduzir o tempo de tarefas de segundos ou minutos para milissegundos, e muitas vezes a API também fica mais limpa e os logs mais fáceis de ler
  • Quanto mais experiência acumulo, mais sinto que programadores demais se preocupam com pequenas unidades de “código bonito”, mas não se preocupam o suficiente com o design da base de código como um todo
    Funções concisas e bem lapidadas são boas, mas textos assim podem virar discussões improdutivas de bike-shedding em PRs ou debates
    Se a função está um pouco bagunçada, onde colocar if e for, se usar map e filter, não me importo muito, desde que o nome da função seja adequado, a interface e os tipos sejam expressivos, o propósito seja claro, haja documentação e os efeitos colaterais não sejam usados em excesso

    • “Mover o if para cima” não é uma discussão de bike-shedding; pelo contrário, como foi dito, está mais próximo de uma escolha arquitetural que precisa ser decidida
      Se pensarmos em onde validar quando há valores de entrada, segundo essa heurística a validação deve acontecer no ponto mais alto em que a entrada é recebida
      Isso também ajuda a entender o código e, do ponto de vista de prova, é tecnicamente necessário, pois as pré-condições precisam ser propagadas para cima
      O primeiro conselho definitivamente não é uma discussão trivial; o segundo é um pouco mais ambíguo
    • Trabalhando com segurança, tive muito contato com tech leads e arquitetos por volta de L7~L9 segundo os critérios da FAANG, e as pessoas mais obcecadas por “design correto” geralmente eram aquelas com 5 a 10 anos de carreira
      Elas tentam evitar que juniores deem um tiro no próprio pé, mas muitas vezes ainda não têm senso suficiente do custo da complexidade e de como o código muda no longo prazo
      Por outro lado, depois de mais de 20 anos, a maioria passa a valorizar simplicidade acima da maior parte dos elementos técnicos
      Perguntas como “quão cedo devo ramificar?” e “o que este código realmente precisa fazer?” tendem a dar as respostas mais valiosas no longo prazo
      Perguntas sobre abstração e encapsulamento, por sua vez, tendem a levar ao tipo de discussão mencionado acima, e o fato de a maioria dos problemas de segurança aparecer no código de pessoas que só veem o “panorama geral” ocorre justamente porque elas não entendem bem o que aquela base de código supostamente bem projetada faz de fato
    • Se princípios aparentemente pequenos como esses estiverem definidos de antemão, o mesmo tipo de discussão desgastante em PRs e debates pode diminuir
      Em software sensível a desempenho, no qual design orientado a dados se encaixa bem, é preciso prestar mais atenção a essas pequenas unidades. É assim que as otimizações do compilador funcionam
      Então as regras do jogo mudam. O significado das instruções passa a importar, código autoexplicativo passa a ter mais valor, comentários de código ficam apenas para fins de raciocínio, e a “documentação” se aproxima de especificações e manuais de usuário
    • Também vi bastante o oposto. Há muitas pessoas que gostam de projetar catedrais superengenheiradas, mas não pensam na eficiência de baixo nível dos algoritmos
    • Um motivo é que as linguagens de programação não têm formas suficientes de expressar de maneira significativa componentes muito maiores que funções, ou componentes que se conectam de modo diferente das funções
      No máximo dá para agrupar funções; depois disso, é cada um por si
  • “Mover o if para cima” tem a desvantagem de que pré-condições e pós-condições não ficam diretamente visíveis dentro da definição da função, e precisam ser verificadas em cada ponto de chamada
    Em grandes projetos com várias pessoas, uma função assim pode ser reutilizada fora do contexto pretendido e levar a bugs
    Usar um framework de contratos resolveria isso, mas você acabaria escrevendo as condições duas vezes, no contrato e no código; com tipos dependentes acontece o mesmo
    É interessante a abordagem de marcar uma região de código pertencente a um contexto específico e definir funções que só podem ser chamadas dentro desse contexto
    Em Python, seria possível usar um decorator como @requires_context("VALIDATED_XY") e um context manager validated_xy para fazer a função ser chamada apenas em uma região validada
    O runtime não conhece o significado desse contexto, mas, com ferramentas e testes, dá para projetar de modo que o contexto desejado só seja estabelecido quando determinadas condições forem satisfeitas
    Em linguagens como Haskell, isso poderia ser imposto no nível de tipos com algo como a mônada identidade; mesmo sem imposição no nível de tipos, pode ser uma forma interessante de proteger áreas de código “unsafe”

    • Acho que você deixou passar a segunda parte do argumento do autor. A ideia é “empurrar a checagem de pré-condições para o chamador e impô-la por tipos
      Portanto, as pré-condições continuam diretamente visíveis na definição da função, só que aparecem como parte da assinatura de tipo, não como uma instrução if
      Esse é um padrão comum em Rust, usado no texto, e, diferentemente de uma checagem com if, é uma pré-condição estrita verificada em tempo de compilação, não em tempo de execução; se ela não for satisfeita, o programa não compila
    • Também é possível expressar isso com tipos em Python
      Por exemplo, receber algo como do_something(position: ValidatedPosition) e, depois de validar um Position comum, convertê-lo para ValidatedPosition antes de passá-lo adiante
      Na prática, você encapsularia a validação dentro do construtor de ValidatedPosition, mas o ponto central é que, se você tentar passar um Position diretamente, o mypy avisará que foi passado o tipo errado
      A verificação de tipos do Python não é tão abrangente quanto a do Rust, mas está se tornando cada vez mais útil quando queremos garantir que os dados transmitidos tenham sido tratados adequadamente
    • O conselho de “mover o if para cima” não é sobre checagem de pré-condições, e sim sobre escolher o caminho correto do código
      Se uma função tem pré-condições, é claro que você pode afirmá-las no início da função. Por exemplo, como o sistema de tipos de Java permite null, uma função que precisa de um objeto deve lançar uma exceção se receber null
      Cada ponto de chamada tem a responsabilidade de chamar a função apenas quando as pré-condições forem verdadeiras. Isso é natural pela própria definição de pré-condição
      Chamar uma função violando suas pré-condições é um bug do chamador. Pode ser necessário haver código dentro da função para verificar isso e evitar comportamento indefinido, mas é preciso distinguir essas asserções do fluxo de controle real do programa, e o texto trata deste último
    • No exemplo fn frobnicate(walrus: Walrus), o programa não compila se você passar algo que não seja um Walrus possuído
      Mesmo que seja genérico, o argumento precisa satisfazer os limites de trait, e, dependendo de como o argumento é usado dentro da função, o compilador exige os limites necessários na definição da função
    • Tenho a impressão de que a intenção de public e private era, em certa medida, servir para marcar o código de acordo com um contexto específico
      Ou talvez seja necessária uma semântica mais específica que atravesse as áreas cobertas por public, private e protected no ecossistema .NET
  • O primeiro exemplo não é ruim por causa do if e do for, mas por outro motivo
    Em geral, quando existe um “contêiner” de alguma coisa, é melhor escrever a função para o “objeto” de nível de domínio que está dentro dele, e não para o contêiner
    Em Clojure, ao usar um agent, você não escreve funções para o agent; escreve funções para o tipo de objeto que o agent pode conter
    Em Elixir também, as funções centrais de domínio operam sobre a estrutura de dados de domínio interna, não sobre o PID, e quando necessário a chamada ao GenServer é delegada a elas
    Isso torna tudo mais flexível e permite separar de forma mais limpa o domínio central, “fazer frobnicate em um Walrus”, da preocupação da aplicação, “pode haver ou não um Walrus e, se houver, fazer frobnicate nele”

    • O conselho dado parece ter o risco de empurrar a lógica de validação para cima demais
      Validar no início é bom, mas também é importante emitir claramente um erro de validação em vez de deixar a função explodir de repente com um erro estranho
      Haskell resolve isso com newtype, oferecendo um “contêiner transparente que certifica que a validação adequada já foi feita”
      O conselho que eu realmente gostaria de enfatizar para as pessoas é preferir o “if triste”. Em vez de aninhar o caminho normal em várias camadas, quase sempre é mais legível e mais fácil de manter verificar as condições inválidas uma a uma no topo e corrigi-las ou interromper imediatamente
      As pessoas têm a tendência natural de se concentrar no caso esperado, mas o código, ao contrário, quer que se dê atenção primeiro aos casos excepcionais. Todo if cria uma carga mental e, se for preciso buscar informações em um sistema externo ou encerrar cedo por erro, dá para se livrar imediatamente dessa carga quando a detecção e o tratamento ficam juntos
    • Se a ideia é que o primeiro exemplo seria melhor como walrus.frobnicate(), isso me parece mais uma questão de preferência sintática do que o ponto central que o autor quer defender
  • Eu não diria que isso é necessariamente um mau conselho, mas também não é necessariamente um bom conselho
    O fato de a linguagem escolhida ser Rust diz bastante. Um sistema de tipos forte evita muita programação defensiva que é necessária em outras linguagens
    Se um programador C não verificar a validade de um ponteiro passado para uma função e causar uma desreferência de NULL, eu não gostaria de trabalhar na mesma equipe que essa pessoa
    Então pelo menos alguns if certamente devem ficar mais embaixo, e é bom que os erros sejam propagados corretamente para cima
    Sobre for, não tenho uma opinião tão forte, mas em C, como parâmetros de array decaem para ponteiros, acho que a iteração também deve ficar em cima, não embaixo. Na função em que o array foi criado, dá para confiar no tamanho; na função para a qual ele foi passado como argumento, não

    • Interfaces em C devem documentar cuidadosamente as expectativas e verificar apenas exatamente isso
      A documentação deve substituir um sistema de tipos forte; verificações em tempo de execução não devem fazer esse papel
      Código cheio de verificações de NULL e outras medidas defensivas é muito mais difícil de ler
      Dá para dizer que, em fronteiras de bibliotecas, verificações mais defensivas são necessárias, e este texto está dizendo exatamente para empurrar essas verificações para cima
      Código crítico para segurança pode ser diferente, mas na maioria dos casos basta que desreferências acidentais de NULL sejam capturadas por testes, sanitizers e fuzzing
    • Em C, NULL muitas vezes é um valor de ponteiro válido, mas que não pode ser desreferenciado
      Para verificar valores de ponteiro inválidos, seria preciso verificar todo o enorme conjunto de possíveis valores incorretos; se você verifica apenas NULL, na prática não está verificando valores inválidos
      Se a pré-condição da função for “o parâmetro p não pode ser NULL”, tudo bem verificar. Mas se a pré-condição for “p deve ser um ponteiro válido”, boa sorte para encontrar uma asserção adequada
    • Minha heurística é: se o sistema de tipos não impede valores inválidos, então há responsabilidade de bloqueá-los em tempo de execução
      Hoje em dia uso bastante T-SQL, e não é possível declarar parâmetros ou variáveis como NOT NULL
      Por isso é melhor verificar NULL o mais cedo que for razoavelmente possível, normalmente no topo da stored procedure
      Caso contrário, NULL pode se propagar inesperadamente para dentro da hierarquia de chamadas e criar problemas menos claros
      Felizmente, dados de tabelas podem ser declarados como NOT NULL, então esse tipo de bug normalmente não corrompe os dados, mas é muito mais fácil capturá-lo cedo
      Porém, se existe uma lógica que escreve no DB dependendo de determinado valor de parâmetro e esse valor inesperadamente é NULL, um valor errado pode ser escrito, ou um valor necessário pode deixar de ser escrito, o que na prática pode corromper os dados
      Portanto, programação defensiva é a resposta
  • Sem o contexto adequado, esse conselho parece bem estranho e talvez até seja um mau conselho
    Laços e instruções if são ambos operações de fluxo de controle, então parte do argumento do texto não faz muito sentido
    O argumento mais forte parece ser desempenho, mas desempenho geralmente deveria ser uma das últimas preocupações, especialmente quando se trata de conselhos heurísticos
    Infelizmente, o autor transformou isso em um bordão, e espero que não se espalhe

    • Desempenho pode ser um argumento, mas não é um argumento forte
      Se for possível escrever o código melhorado como no exemplo do autor, a condição é constante durante a execução do laço. Se não for uma condição cara sendo avaliada a cada vez, a predição de desvio dá conta da maior parte
      Se a condição for uma expressão booleana formada por valores const, talvez o compilador também consiga descobrir
  • Achei que o texto inteiro seguiria para este tipo de exemplo: dividir walrus de acordo com a condição e chamar frobnicate_batch(fwalrus) e transmogrify_batch(twalrus)
    Mas, na prática, ele foi para um exemplo em que um único condicional envolve o laço inteiro em dois ramos

  • É surpreendente que programadores vindos de uma formação em engenharia de software errem isso com frequência
    Eu comecei a programar na área científica, e lá é absolutamente necessário pensar nessas coisas
    Só de escolher mal a ordem dos laços, uma simulação pode levar 1 semana em vez de 1 hora
    Por causa desse contexto, acabo fazendo instintivamente pequenas otimizações ao definir a ordem adequada de for e if, e código que não faz isso simplesmente parece errado

  • Não tenho certeza de que uma regra geral como essa possa realmente ser aplicada a código real
    Regras desse tipo muitas vezes parecem dogmas mal colocados, e, mesmo que um post de blog comece dizendo que é uma heurística, programadores mais jovens nem sempre a recebem assim
    Algumas semanas atrás, o YouTube continuou me recomendando um vídeo chamado “I'm a never-nester”, que parecia defender que nunca se deve aninhar if, e achei bem ridículo
    Como no exemplo do texto, ele marcava como “ruim” um código com if condition dentro de um for, mas, na maior parte do código escrito de fato desse jeito, condition depende de walrus, então não dá para mover o if para cima
    Se desse, o fato de a mesma condição ser reavaliada a cada iteração seria tão óbvio que programadores tenderiam naturalmente a evitar isso
    Mas, quando um júnior ou estudante lê um conselho que soa como dogma, pode acabar produzindo um código pior ao tentar segui-lo rigidamente

    • Não sou particularmente dogmático quanto a “nunca aninhar”, mas, supondo que a sintaxe permita, nunca vi empiricamente um caso em que separar os quatro casos com match (condition_a, condition_b) não fosse melhor do que if aninhados
    • Vejo isso, na verdade, como um bom exemplo de onde o if deve ser “movido para cima”
      O objetivo do código é executar uma certa operação em walrus dependendo da condição, mas, na prática, o if está sendo usado no lugar de polimorfismo e do sistema de tipos
      Por que walrus deveria ter duas funções que precisam ser chamadas em cenários diferentes? Por que não ter uma única função e dois tipos, e passar o tipo correto?
      Mesmo na estrutura atual, é possível escolher a função de processamento lá em cima com base na condição e fazer o laço abaixo chamar a função escolhida a cada vez
      Se a decisão for tomada o mais cedo possível, não será preciso espalhá-la pelo código inteiro. O interior do código executa sempre a mesma tarefa, sem ramificações, e a saída só varia por meio do grafo de configuração
      Claro que isso não é uma ideia nova; 15 anos atrás já era uma ideia antiga: https://www.youtube.com/watch?v=4F72VULWFvc
    • A refatoração “GOOD” só funciona quando a condição não depende de walrus, e ajuda a deixar esse fato explícito
      Se aplicar novamente aqui o “mova o for para baixo”, a forma resultante será chamar frobnicate_batch(walruses) ou transmogrify_batch(walruses) dentro de if condition
    • Acho bom que textos assim existam
      Alguém colocou em palavras um problema que encontrei várias vezes, mas que eu não conseguia expressar direito, e parece um bom modelo para guardar na cabeça
      Ao mesmo tempo, essa crítica também tem valor. Espero que desenvolvedores juniores dogmáticos leiam junto e passem a julgar com um pouco mais de nuance
    • O ponto essencial a aprender com toda “heurística” e “boa prática” é o motivo por trás dela
      Programadores, em especial, não devem copiar essas coisas e aplicá-las mecanicamente
      Aplicar heurísticas e boas práticas às cegas pode não ser uma boa ideia e, no fim, tudo “depende do contexto”
  • Heurísticas como essa geralmente contêm um grão de sabedoria, mas, em geral, acabam criando algo que depois precisa ser desdogmatizado em programadores iniciantes
    Sempre há muitos casos em que as coisas pioram quando se tenta segui-las com rigidez demais, e “saber quando não ouvir esse conselho” é, na prática, a parte difícil central

    • A desdogmatização é necessária de qualquer forma
      Acho que essas regras são úteis para brincar com elas. Experimente adotá-las, leve-as até o fim, inverta-as por um dia ou por um ano, e veja até onde chegam
      Aprender seus limites também é mais um material acumulado sobre o pergaminho que continua recebendo camadas
    • Este texto pode ser útil como um koan dentro de uma coleção maior
      Alguns desses koans devem se contradizer