Por que as declarações "if elif else" praticamente nunca estão no formato de tabela?

73
if   i>0 : return sqrt(i)  
elif i==0: return 0  
else     : return 1j * sqrt(-i)

VS

if i>0:  
   return sqrt(i)  
elif i==0:  
   return 0  
else:  
   return 1j * sqrt(-i)  

Dados os exemplos acima, não entendo por que praticamente nunca vejo o primeiro estilo nas bases de código. Para mim, você transforma o código em um formato tabular que mostra claramente o que você deseja. A primeira coluna pode ser virtualmente ignorada. A segunda coluna identifica a condição e a terceira coluna fornece a saída desejada. Parece, pelo menos para mim, direto e fácil de ler. No entanto, eu sempre vejo esse tipo simples de situação de caso / troca sair no formato estendido e recuado por tabulação. Por que é que? As pessoas acham o segundo formato mais legível?

O único caso em que isso pode ser problemático é se o código for alterado e ficar mais longo. Nesse caso, acho perfeitamente razoável refatorar o código no formato longo e recuado. Todo mundo faz isso da segunda maneira, simplesmente porque é do jeito que sempre foi feito? Sendo um advogado do diabo, acho que outro motivo pode ser porque as pessoas acham dois formatos diferentes, dependendo da complexidade das declarações if / else, que são confusas? Qualquer insight seria apreciado.

horta
fonte
91
Porque as pessoas acham a segunda opção mais legível?
GrandmasterB
65
O caso de uso de ramificações mutuamente exclusivas que todos retornam um valor do mesmo tipo não aparece frequentemente em idiomas imperativos, em comparação com ramificações que podem não retornar valores, podem abranger várias linhas e provavelmente têm efeitos colaterais. Se você analisasse as linguagens de programação funcionais, veria um código que se assemelha ao seu primeiro exemplo com muito mais frequência.
Doval
47
@horta "O único caso em que isso pode ser problemático é que o código muda e fica mais longo." - Você NUNCA deve assumir que um pedaço de código não será alterado. A refatoração de código ocupa uma grande maioria do ciclo de vida de um software.
Charles Addis
7
@horta: Nada a ver comigo. É o código. No contexto da base de código que estou lendo, quero ver se as instruções (e outras construções de linguagem) estão formatadas de maneira consistente, sem nenhum caso de borda. Não que eu tenha aprendido a ler código de uma maneira específica, posso ler muito bem, mas é mais legível se tudo for o mesmo. Novamente, não é o mesmo para mim, o mesmo para o restante do código.
GManNickG
44
Além disso, a maioria dos depuradores é baseada em linha. Não é possível colocar um ponto de interrupção em uma instrução que esteja dentro de um ifse estiver na mesma linha.
Isanae

Respostas:

93

Um motivo pode ser que você não está usando idiomas populares.

Alguns contra-exemplos:

Haskell com guardas e com padrões:

sign x |  x >  0        =   1
       |  x == 0        =   0
       |  x <  0        =  -1

take  0     _           =  []
take  _     []          =  []
take  n     (x:xs)      =  x : take (n-1) xs

Erlang com padrões:

insert(X,Set) ->
    case lists:member(X,Set) of
        true  -> Set;
        false -> [X|Set]
    end.

Emacs lisp:

(pcase (get-return-code x)
  (`success       (message "Done!"))
  (`would-block   (message "Sorry, can't do it now"))
  (`read-only     (message "The shmliblick is read-only"))
  (`access-denied (message "You do not have the needed rights"))
  (code           (message "Unknown return code %S" code)))

Geralmente, vejo que o formato da tabela é bastante popular entre as linguagens funcionais (e, em geral, baseadas em expressões), enquanto a quebra de linhas é mais popular em outras (principalmente baseada em instruções).

viraptor
fonte
10
Votei positivo e concordo geralmente, mas sinto-me obrigado a salientar que 1. Todos esses são retornos triviais 2. Os haskellers gostam de identificadores comicamente curtos, além da dispersão da própria linguagem e 3. As linguagens funcionais tendem a ser baseadas em expressões , e mesmo linguagens imperativas têm padrões diferentes para expressões e declarações. Se o OP reescreveu o exemplo original como aplicações de função que ele poderia obter aconselhamento diferente ...
Jared Smith
3
@JaredSmith Obrigado pela expressão / declaração baseada em divisão - acho que pode ser ainda mais adequado do que funcional / imperativo. Mais uma vez, o ruby ​​é quase baseado em expressões e não usa essa convenção com frequência. (exceções a tudo) Em relação aos pontos 1 e 2, considero 50% + do código Haskell real como "retornos triviais" que são apenas partes de algo maior - é exatamente como esse código é escrito - não apenas nos exemplos aqui. Por exemplo, perto da metade das funções aqui estão apenas um / dois forros. (algumas linhas de usar o layout tabela)
viraptor
Sim. Eu não sou um Haskeller, mas faço um pouco de ocaml e acho que a correspondência de padrões tende a ser muito mais concisa do que os switches logicamente equivalentes, e as funções polimórficas cobrem muito disso. Imagino que as classes de tipo de Haskell aumentariam ainda mais essa cobertura.
Jared Smith
Eu acho que é a sintaxe do caso padrão que promove isso. Como é mais conciso e geralmente mais próximo de um pequeno gabinete, é mais fácil expressar como uma linha. Freqüentemente faço isso com instruções curtas de casos de troca por razões semelhantes. Porém, if-elsedeclarações literais ainda estão espalhadas por várias linhas, quando não são efetivamente um simples ternário.
Isiah Meadows
@viraptor Tecnicamente, os outros 50% - do código haskell são "retornos não triviais" porque todas as funções do haskell são funcionalmente puras e não podem ter efeitos colaterais. Até funções que lêem e imprimem na linha de comando são apenas longas declarações de retorno.
Pharap
134

É mais legível. Algumas razões pelas quais:

  • Quase todas as linguagens usam essa sintaxe (nem todas, a maioria - seu exemplo parece ser Python)
  • isanae apontou em um comentário que a maioria dos depuradores é baseada em linha (não em declaração)
  • Ele começa a parecer ainda mais feio se você precisar alinhar ponto e vírgula ou chaves
  • Lê de cima para baixo mais suavemente
  • Parece terrivelmente ilegível se você tiver algo além de declarações de retorno triviais
    • Qualquer sintaxe significativa de recuo é perdida quando você desliza o código, pois o código condicional não é mais visualmente separado (de Dan Neely )
    • Isso será particularmente ruim se você continuar a corrigir / adicionar itens nas instruções if de 1 linha
  • Só é legível se todas as suas verificações se tiverem o mesmo comprimento
  • Isso significa que você não pode formatar complicado se as declarações em declarações com várias linhas , elas precisarão ser oneliners
  • É muito mais provável que eu observe bugs / fluxo lógico ao ler verticalmente linha por linha, sem tentar analisar várias linhas juntas
  • Nossos cérebros leem texto mais estreito e alto MUITO mais rápido que o texto horizontal longo

No minuto em que você tentar fazer isso, você será reescrito em instruções de várias linhas. O que significa que você acabou de perder tempo!

Além disso, as pessoas inevitavelmente adicionam algo como:

if i>0:  
   print('foobar')
   return sqrt(i)  
elif i==0:  
   return 0  
else:  
   return 1j * sqrt(-i)  

Não é muito frequente fazer isso antes de você decidir que esse formato é muito melhor que sua alternativa. Ah, mas você pode incorporar tudo em uma linha! enderland morre por dentro .

Ou isto:

if   i>0 : return sqrt(i)  
elif i==0 and bar==0: return 0  
else     : return 1j * sqrt(-i)

O que é muito, muito chato. Ninguém gosta de formatar coisas assim.

E por último, você começará a guerra santa do problema de "quantos espaços para abas". O que é renderizado perfeitamente na tela como formato de tabela pode não renderizar na minha, dependendo das configurações.

A legibilidade não deve depender das configurações IDE de qualquer maneira.

enderland
fonte
14
@horta porque você terá que convertê- lo se você formatá-lo da primeira maneira, inicialmente? Meu objetivo é minimizar o trabalho para o futuro da Terra. Criar tabelas de espaço em branco bonitas e contar espaços e guias para atualizar a formatação visual quando eu puder adicionar alguma lógica a uma verificação se não for divertida (ignorando as implicações de legibilidade).
Enderland
14
@horta Ele não está necessariamente dizendo que não iria consertar, está dizendo que precisaria consertá-lo, e isso leva muito tempo gasto em formatação tediosa, em vez de em programação.
Servy
11
@horta: IMHO seu caminho geralmente é menos legível e é definitivamente muito mais irritante para formatar. Além disso, ele só pode ser usado quando os "ifs" são pequenos liners, o que raramente é um caso. Por fim, de alguma forma não é legal, IMHO, carregar dois tipos de "se" formatações por causa disso.
26416 dagnelies
9
@horta, você parece ser abençoado por trabalhar em sistemas em que requisitos, sistemas, APIs e usuários nunca mudam. Sua alma sortuda.
Enderland
11
Eu acrescentaria: qualquer pequena alteração em uma única condição pode exigir a reformatação das outras para corresponder à :posição -> fazendo uma diferença no CVS, de repente se torna mais difícil entender o que realmente está mudando. Isso também é válido para condição versus corpo. Tê-los em linhas separadas significa que, se você alterar apenas um deles, as diferenças mostram claramente que apenas a condição mudou, não o corpo.
Bakuriu 26/07/16
55

Acredito firmemente em 'o código é lido muitas vezes, escrito poucas - então a legibilidade é muito importante'.

Uma coisa importante que me ajuda quando leio o código de outras pessoas é que segue os padrões 'normais' que meus olhos são treinados para reconhecer. Consigo ler a forma recuada com mais facilidade porque já a vi tantas vezes que é registrada quase automaticamente (com pouco esforço cognitivo da minha parte). Não é porque é 'mais bonita' - é porque segue as convenções com as quais estou acostumado. Convenção supera 'melhor' ...

Art Swri
fonte
3
Relevante: thecodelesscode.com/case/94
Kevin
11
Isso explica por que as pessoas são conservadoras. Não explica por que as pessoas escolheram escrever seu código de uma certa maneira para começar.
Jørgen Fogh
8
A pergunta era 'por que vejo isso com frequência', e não de onde veio esse estilo. Ambas as perguntas são interessantes; Eu tentei responder o que eu pensei que estava sendo perguntado.
Art SWRI
16

Juntamente com as outras desvantagens já mencionadas, o layout tabular aumenta as chances de conflitos de mesclagem de controle de versão que requerem intervenção manual.

Quando um bloco de código alinhado tabularmente precisa ser realinhado, o sistema de controle de versão tratará cada uma dessas linhas como tendo sido modificadas:

diff --git a/foo.rb b/foo.rb
index 40f7833..694d8fe 100644
--- a/foo.rb
+++ b/foo.rb
@@ -1,8 +1,8 @@
 class Foo

   def initialize(options)
-    @cached_metadata = options[:metadata]
-    @logger          = options[:logger]
+    @metadata = options[:metadata]
+    @logger   = options[:logger]
   end

 end

Agora, suponha que nesse meio tempo, em outro ramo, um programador tenha adicionado uma nova linha ao bloco de código alinhado:

diff --git a/foo.rb b/foo.rb
index 40f7833..86648cb 100644
--- a/foo.rb
+++ b/foo.rb
@@ -3,6 +3,7 @@ class Foo
   def initialize(options)
     @cached_metadata = options[:metadata]
     @logger          = options[:logger]
+    @kittens         = options[:kittens]
   end

 end

A fusão desse ramo falhará:

wayne@mercury:/tmp/foo$ git merge add_kittens
Auto-merging foo.rb
CONFLICT (content): Merge conflict in foo.rb
Automatic merge failed; fix conflicts and then commit the result.

Se o código que está sendo modificado não tivesse usado o alinhamento tabular, a mesclagem seria bem-sucedida automaticamente.

(Essa resposta foi "plagiada" do meu próprio artigo, desencorajando o alinhamento tabular no código).

Wayne Conrad
fonte
11
Interessante, mas isso não é uma falha nas ferramentas de mesclagem? Especificamente git neste caso? Este é um ponto de dados para a convenção, sendo o caminho mais fácil. Para mim, é algo que pode ser aprimorado (do lado da ferramenta).
horta 28/07
7
@horta Para que a ferramenta de mesclagem modifique o espaço em branco de uma maneira que nem sempre quebra o código, ela precisará entender de que maneira pode modificar o espaço em branco sem alterar o significado do código. Também terá que entender o alinhamento tabular específico que está sendo usado. Isso não dependerá apenas da linguagem (Python!), Mas provavelmente exigirá que a ferramenta entenda o código até certo ponto. Por outro lado, a mesclagem baseada em linha pode ser feita sem a IA e, muitas vezes, nem quebra o código.
Wayne Conrad
Peguei vocês. Portanto, como mencionado nos comentários, até termos IDEs ou formatos de entrada de programação que incorporem tabelas diretamente no formato, os problemas de ferramentas sempre estarão lá apenas dificultando a vida de quem prefere tabelas.
horta
11
@horta Correct. A maioria das minhas objeções ao alinhamento tabular no código pode desaparecer com ferramentas suficientemente avançadas.
Wayne Conrad
8

Os formatos de tabela podem ser muito bons se as coisas sempre se ajustarem à largura atribuída. Se algo exceder a largura alocada, no entanto, muitas vezes será necessário ter parte da tabela que não esteja alinhada com o restante ou ajustar o layout de tudo o mais na tabela para ajustar-se ao item longo .

Se os arquivos de origem forem editados usando programas projetados para trabalhar com dados em formato de tabela e puderem lidar com itens muito longos usando um tamanho de fonte menor, dividindo-os em duas linhas na mesma célula, etc., pode fazer sentido usar tabular formatos com mais frequência, mas a maioria dos compiladores deseja arquivos de origem livres dos tipos de marcação que esses editores precisariam armazenar para manter a formatação. Usar linhas com quantidades variáveis ​​de recuo, mas nenhum outro layout não é tão bom quanto a formatação tabular no melhor dos casos, mas não causa quase tantos problemas no pior caso.

supercat
fonte
Verdadeiro. Percebi que o editor de texto que uso (vim) tem um suporte horrível para formatação tabular ou mesmo texto amplo. Não vi outros editores de texto se saírem muito melhor.
horta 27/07
6

Existe a declaração 'switch' que fornece esse tipo de coisa para casos especiais, mas acho que não é disso que você está perguntando.

Já vi declarações em formato de tabela, mas deve haver um grande número de condições para que valha a pena. 3 se as instruções são melhor exibidas no formato tradicional, mas se você tivesse 20, é muito mais fácil exibi-las em um grande bloco formatado para torná-lo mais claro.

E aí está o ponto: clareza. Se facilitar a visualização (e seu primeiro exemplo não for fácil de ver onde está o delimitador:), formate-o para se adequar à situação. Caso contrário, atenha-se ao que as pessoas esperam, pois isso é sempre mais fácil de reconhecer.

gbjbaanb
fonte
11
O OP parece estar usando Python, então não switch.
Jared Smith
2
"3 se as instruções são melhor exibidas no formato tradicional, mas se você tivesse 20" ... então você tem problemas maiores em que pensar! :)
Grimm O Opiner
@GrimmTheOpiner Se você estiver escrevendo um analisador de idioma ou um stringifier AST, é uma coisa muito possível de lidar. Por exemplo, uma vez contribuí para um analisador de JavaScript, onde dividi uma função com 15 a 20 casos, um para cada tipo de expressão. Eu segmentava a maioria dos casos para suas próprias funções (para um notável aumento de desempenho), mas o longo switchera uma necessidade.
Isiah Meadows
@JaredSmith: Aparentemente switché mau, mas instanciar um dicionário, em seguida, executando uma pesquisa sobre ele para fazer ramificação trivial não é mau ...
Mark K Cowan
11
@ MarkKCowan oh, eu peguei o sarcasmo, mas pensei que você estava usando isso para zombar de mim. falta de contexto na internet e outros enfeites.
Jared Smith
1

Se sua expressão é realmente fácil, a maioria das linguagens de programação oferece o operador?: Branching:

return  ( i > 0  ) ? sqrt( i)
      : ( i == 0 ) ? 0
        /* else */ : 1j * sqrt( -i )

Este é um formato tabular curto e legível. Mas a parte importante é: vejo de relance qual é a ação "principal". Esta é uma declaração de retorno! E o valor é decidido por certas condições.

Se, por outro lado, você possui ramificações que executam códigos diferentes, acho muito mais legível recuar esses blocos. Porque agora existem ações "principais" diferentes, dependendo da instrução if. Em um caso, jogamos, em um caso, registramos e retornamos ou simplesmente retornamos. Existe um fluxo de programa diferente, dependendo da lógica, portanto, os blocos de código encapsulam os diferentes ramos e os tornam mais proeminentes para o desenvolvedor (por exemplo, leitura rápida de uma função para captar o fluxo do programa)

if ( i > 0 )
{
    throw new InvalidInputException(...);
}
else if ( i == 0 )
{
    return 0;
}
else
{
    log( "Calculating sqrt" );
    return sqrt( -i );
}
Falco
fonte
7
Na verdade, acho o seu "formato tabular de leitura curta" um pesadelo de se ler, enquanto o formato proposto pelo OP é perfeitamente adequado.
Matteo Italia
@MatteoItalia, que tal esta versão editada?
Falco
5
Desculpe, ainda pior; Eu acho que isso vem do fato de que ?e :são mais difíceis de identificar do que as if/ elsekeywords e / ou devido ao "ruído" adicional dos símbolos.
Matteo Italia
@ MatteoItalia: tive casos com mais de cem valores diferentes. O valor da tabela torna possível verificar se há erros. Com várias linhas, é impossível.
gnasher729
11
@ gnasher729 - Para 100 "valores" diferentes, geralmente considero muito melhor declarar uma estrutura de dados de cada "item" e listar tudo isso como uma tabela na inicialização de uma matriz dessas estruturas de dados. (É claro que as limitações de idioma podem ser aplicadas aqui). Se algum item exigir um aspecto "computacional", a estrutura do item poderá conter um ponteiro ou referência a uma função para executar a ação necessária. Para alguns aplicativos, isso pode simplificar bastante o código e facilitar a manutenção.
Michael Karas
1

Como enderland já disse, você está supondo que só tenha um "retorno" como a ação e que possa marcar esse "retorno" no final da condição. Eu gostaria de dar alguns detalhes extras sobre por que isso não será bem-sucedido.

Não sei quais são os seus idiomas preferidos, mas estou codificando em C há muito tempo. Existem vários padrões de codificação em torno dos quais o objetivo é evitar alguns erros de codificação padrão, impedindo construções de código propensas a erros, na codificação inicial ou durante a manutenção posterior. Eu estou mais familiarizado com o MISRA-C, mas existem outros, e geralmente todos eles têm regras semelhantes porque estão lidando com os mesmos problemas no mesmo idioma.

Um erro popular que os padrões de codificação costumam abordar é esse pequeno problema:

if (x == 10)
    do_something();
    do_something_else();

Isso não faz o que você pensa que faz. No que diz respeito a C, se x é 10, então você chama do_something(), mas do_something_else()é chamado independentemente do valor de x . Somente a ação imediatamente após a instrução "if" é condicional. Pode ser o que o codificador pretendia; nesse caso, existe uma armadilha em potencial para os mantenedores; ou pode não ser o que o codificador pretendia; nesse caso, há um erro. É uma pergunta de entrevista popular.

A solução nos padrões de codificação é exigir chaves entre todas as ações condicionais, mesmo que sejam de linha única. Agora temos

if (x == 10)
{
    do_something();
    do_something_else();
}

ou

if (x == 10)
{
    do_something();
}
do_something_else();

e agora funciona corretamente e é claro para os mantenedores.

Você notará que isso é totalmente incompatível com o formato de tabela.

Algumas outras linguagens (por exemplo, Python) analisaram esse problema e decidiram que, como os codificadores estavam usando espaços em branco para deixar o layout claro, seria uma boa idéia usar espaços em branco em vez de chaves. Então, em Python,

if x == 10:
    do_something()
    do_something_else()

faz as chamadas para ambos do_something()e do_something_else()condicional em x == 10, enquanto

if x == 10:
    do_something()
do_something_else()

significa que apenas do_something()é condicional em x e do_something_else()é sempre chamado.

É um conceito válido e você encontrará alguns idiomas para usá-lo. (Eu vi pela primeira vez no Occam2, quando.) Novamente, porém, você pode ver facilmente que o seu formato de tabela é incompatível com o idioma.

Graham
fonte
11
Você não entendeu. O problema de que você fala é um estranho pesadelo não padrão específico do C que causa o problema de que você fala. Se você estiver codificando em C, nunca recomendaria usar o método if simples alternativo com o formato tabular que sugeri. Em vez disso, como você está usando C, você usaria chavetas na mesma linha. Os aparelhos tornariam o formato da tabela ainda mais claro porque eles agem como delimitadores.
horta 27/07
Além disso, as instruções de retorno neste caso são apenas um exemplo. Em geral, isso pode ser o cheiro do código em si. Estou apenas me referindo ao formato de instruções simples, não necessariamente com instruções de retorno.
horta 27/07
2
Meu argumento é que isso torna um formato de tabela ainda mais desajeitado. Aliás, não é específico de C - é compartilhado por todas as linguagens derivadas de C, portanto, C ++, C #, Java e JavaScript permitem a mesma pegada.
Graham
11
Não me importo que sejam declarações de retorno - entendo que sua intenção é mostrar declarações simples. Mas isso se torna mais complicado. E, é claro, assim que qualquer instrução se tornar não simples, você precisará alterar a formatação porque é impossível manter um formato de tabela. A menos que você esteja ofuscando o código, longas linhas de código são um cheiro por si só. (Limite original era 80 caracteres, estes dias é mais tipicamente cerca de 130 caracteres, mas o princípio geral ainda mantém que você não deve ter que se deslocar para ver o fim da linha.)
Graham
1

O layout tabular pode ser útil em alguns casos limitados, mas há poucas vezes em que é útil com if.

Em casos simples?: Pode ser uma escolha melhor. Em casos médios, uma opção geralmente é mais adequada (se o seu idioma tiver uma). Em casos complicados, você pode achar que as tabelas de chamadas são mais adequadas.

Houve muitas vezes em que refatorar o código que eu o reorganizei para ser tabular para tornar seu padrão óbvio. Raramente é o caso que deixo assim, pois na maioria dos casos existe uma maneira melhor de resolver o problema depois que você o entende. Ocasionalmente, uma prática de codificação ou padrão de layout o proíbe; nesse caso, um comentário é útil.

Houve algumas perguntas sobre ?:. Sim, é o operador ternário (ou, como eu gosto de pensar, o valor se). na primeira tentativa de corar esse exemplo é um pouco complicado para?: (e usar em excesso?: não ajuda na legibilidade, mas prejudica), mas com algumas reflexões solução.

if i==0: return 0
return i>0?sqrt(i):(1j*sqrt(-i))
hildred
fonte
11
Você pode precisar deixar claro o que "?:" É para os não iniciados (por exemplo, com um exemplo, possivelmente relacionado à pergunta).
Peter Mortensen
Estou assumindo que esse é o operador ternário. Sinto que isso é evitado pela boa razão de que o operador ternário tende a reorganizar o padrão se, fazer alguma coisa ou fazer outro formato de coisa que as pessoas veem dia após dia e, portanto, possam ler facilmente.
horta 27/07
@ PeterMortensen: Se os não iniciados não souberem o que isso significa, eles devem ficar longe do código até que façam a pergunta óbvia e aprendam.
gnasher729
@horta Ternary é if ? do stuff : do other stuff. Mesma ordem que um if / else.
Navin
11
@ Navin Ah, talvez seja apenas uma falha da linguagem que eu mais uso (python). stackoverflow.com/questions/394809/…
horta
-3

Não vejo nada de errado com o formato da tabela. Preferência pessoal, mas eu usaria um ternário como este:

return i>0  ? sqrt(i)       :
       i==0 ? 0             :
              1j * sqrt(-i)

Não há necessidade de repetir returntodas as vezes :)

Navin
fonte
11
Como mencionado em alguns comentários, as declarações de retorno não são ideais ou o objetivo da publicação, simplesmente um pedaço de código que encontrei on-line e formatado de duas maneiras.
horta 27/07
Ternários Python são do_something() if condition() else do_something_else(), não condition() ? do_something() : do_something_else().
Isiah Meadows
O @IsiahMeadows OP nunca mencionou Python.
Navin