“Fácil de raciocinar” - o que isso significa? [fechadas]

48

Já ouvi muitas vezes quando outros desenvolvedores usam essa frase para "anunciar" alguns padrões ou desenvolver práticas recomendadas. Na maioria das vezes, essa frase é usada quando você está falando sobre os benefícios da programação funcional.

A frase "Fácil de raciocinar" foi usada como está, sem nenhuma explicação ou amostra de código. Então, para mim, torna-se como a próxima palavra-chave "buzz", que os desenvolvedores mais "experientes" usam em suas conversas.

Pergunta: Você pode fornecer alguns exemplos de "Não é fácil raciocinar sobre", então pode ser comparado com exemplos "fáceis de raciocinar"?

    
por Fabio 20.06.2017 / 11:06
fonte

11 respostas

58

Na minha opinião, a frase "fácil de raciocinar" refere-se ao código que é fácil de "executar na sua cabeça".

Ao olhar para um pedaço de código, se ele for curto, claramente escrito, com bons nomes e mínima mutação de valores, então, mentalmente, trabalhar com o que o código faz é uma tarefa (relativamente) fácil.

Um longo código com nomes incorretos, variáveis que mudam constantemente de valor e ramificações complicadas normalmente exigirão, por exemplo, uma caneta e uma folha de papel para ajudar a acompanhar o estado atual. Esse código, portanto, não pode ser facilmente trabalhado apenas em sua cabeça, então tal código não é fácil de raciocinar.

    
por 20.06.2017 / 11:13
fonte
47

É fácil raciocinar sobre um mecanismo ou código quando você precisa levar em consideração algumas coisas para prever o que ele fará, e as coisas que você precisa levar em conta ficam facilmente disponíveis.

Funções verdadeiras sem efeitos colaterais e sem estado são fáceis de raciocinar porque a saída é completamente determinada pela entrada, que está bem nos parâmetros.

Por outro lado, um objeto com estado é muito mais difícil de raciocinar, porque você precisa levar em conta em que estado o objeto está quando um método é chamado, o que significa que você precisa pensar em outras situações que poderiam levar ao objeto. estar em um estado particular.

Ainda pior são as variáveis globais: para raciocinar sobre o código que lê uma variável global, você precisa entender onde, no seu código, essa variável pode ser definida e por quê - e pode nem ser fácil encontrar todos esses locais.

A coisa mais difícil de se raciocinar é a programação multithread com estado compartilhado, porque você não apenas tem estado, você tem vários threads mudando ao mesmo tempo, então raciocine sobre o que um pedaço de código faz quando executado por um thread você tem que admitir a possibilidade de que, em cada ponto de execução, algum outro thread (ou vários deles!) possa estar executando qualquer outra parte do código e alterar os dados nos quais você está operando. olhos. Em teoria, isso pode ser gerenciado com mutexes / monitores / seções críticas / o que quer que você chame, mas na prática nenhum mero humano é realmente capaz de fazer isso de forma confiável a menos que eles limitem drasticamente o estado compartilhado e / ou paralelismo a muito pequeno seções do código.

    
por 20.06.2017 / 14:41
fonte
9

No caso da programação funcional, o significado de "Fácil de raciocinar" é principalmente que é determinístico. Com isso, quis dizer que uma dada entrada sempre levará à mesma saída. Você pode fazer o que quiser com o programa, contanto que você não toque nesse pedaço de código, ele não vai quebrar.

Por outro lado, o OO é normalmente mais difícil de avaliar porque a "saída" produzida depende do estado interno de cada objeto envolvido. A maneira típica com que se manifestam são inesperados efeitos colaterais : ao alterar uma parte do código, uma parte aparentemente não relacionada é quebrada.

... a desvantagem da programação funcional é claro que, na prática, muito do que você quer fazer é IO e gerenciar o estado.

No entanto, há muitas outras coisas que são mais difíceis de raciocinar, e eu concordo com a @Kilian que a concorrência é um excelente exemplo. Sistemas distribuídos também.

    
por 20.06.2017 / 13:24
fonte
6

Evitando discussões mais amplas e abordando a questão específica:

Can you provide some examples of "Not easy to reason about", so it can be compared with "Easy to reason about" examples?

Eu o encaminho para "A história de Mel, um programador real" , um pedaço do folclore programador que remonta a 1983 e, portanto, conta como "lenda", para a nossa profissão.

Conta a história de um programador que escreve um código que prefere técnicas arcanas sempre que possível, incluindo código auto-referencial e auto-modificador, e exploração deliberada de erros de máquina:

an apparent infinite loop had in fact been coded in such a way as to take advantage of a carry-overflow error. Adding 1 to an instruction that decoded as "Load from address x" normally yielded "Load from address x+1". But when x was already the highest possible address, not only did the address wrap around to zero, but a 1 was carried into the bits from which the opcode would be read, changing the opcode from "load from" to "jump to" so that the full instruction changed from "load from the last address" to "jump to address zero".

Este é um exemplo de código que é "difícil de avaliar".

Claro, Mel discordaria ...

    
por 21.06.2017 / 13:58
fonte
5

Eu posso fornecer um exemplo e um muito comum.

Considere o seguinte código C #.

// items is List<Item>
var names = new List<string>();
for (var i = 0; i < items.Count; i++)
{
    var item = items[i];
    var mangled = MyMangleFunction(item.Name);
    if (mangled.StartsWith("foo"))
    {
        names.Add(mangled);
    }
}

Agora considere esta alternativa.

// items is List<Item>
var names = items
    .Select(item => MyMangleFunction(item.Name))
    .Where(s => s.StartsWith("foo"))
    .ToList();

No segundo exemplo, sei exatamente o que esse código está fazendo de imediato. Quando vejo Select , sei que uma lista de itens está sendo convertida em uma lista de outra coisa. Quando vejo Where , sei que determinados itens estão sendo filtrados. De relance, eu posso entender o que é names e fazer uso efetivo dele.

Quando vejo um loop for , não tenho ideia do que está acontecendo com ele até que eu realmente leia o código. E às vezes eu tenho que rastreá-lo para ter certeza de que eu considerei todos os efeitos colaterais. Eu tenho que fazer um pouco de trabalho para chegar a entender quais são os nomes (além da definição do tipo) e como usá-los efetivamente. Assim, o primeiro exemplo é mais difícil de raciocinar do que o segundo.

Por fim, ser fácil raciocinar aqui também depende da compreensão dos métodos LINQ Select e Where . Se você não os conhece, então o segundo código é mais difícil de avaliar inicialmente. Mas você paga apenas o custo para entendê-los uma vez. Você paga o custo para entender um loop for toda vez que usar um e novamente toda vez que ele mudar. Às vezes o custo vale a pena pagar, mas geralmente ser "mais fácil de raciocinar" é muito mais importante.

    
por 21.06.2017 / 17:09
fonte
2

Uma frase relacionada é (parafraseio),

It's not enough for code to have "no obvious bugs": instead, it should have "obviously no bugs".

Um exemplo de "relativamente fácil de raciocinar" pode ser RAII .

Outro exemplo pode estar evitando abraçar mortalmente : se você pode manter um bloqueio e adquirir outro bloqueio, e há muitos de fechaduras, é difícil ter certeza de que não há cenário em que possa ocorrer um abraço mortal. Adicionar uma regra como "há apenas um bloqueio (global)" ou "você não pode adquirir um segundo bloqueio enquanto mantém um primeiro bloqueio", torna o sistema relativamente fácil de avaliar.

    
por 20.06.2017 / 19:48
fonte
2

O ponto crucial da programação é a análise de casos. Alan Perlis comentou sobre isso no Epigrama 32: Os programadores não devem ser medidos por sua engenhosidade e lógica, mas pela completude de sua análise de casos.

Uma situação é fácil de raciocinar se a análise do caso for fácil. Isso quer dizer que há poucos casos a serem considerados ou, na falta deles, poucos casos especiais - pode haver alguns grandes espaços de casos, mas que colapsam devido a algumas regularidades ou sucumbem a uma técnica de raciocínio. como indução.

Uma versão recursiva de um algoritmo, por exemplo, é geralmente mais fácil de raciocinar do que uma versão imperativa, porque não contribui com casos supérfluos que surgem através da mutação de variáveis de estado de suporte que não aparecem na versão recursiva. . Além disso, a estrutura da recursão é tal que se encaixa em um padrão matemático de prova por indução. Não precisamos considerar complexidades como variantes de loop e pré-condições estritas mais fracas e outras coisas.

Outro aspecto disso é a estrutura do espaço do caso. É mais fácil raciocinar sobre uma situação que tem uma divisão plana, ou quase sempre plana, em comparação a uma situação de caso hierárquica: casos com sub-casos e sub-sub-casos e assim por diante.

Uma propriedade de sistemas que simplifica o raciocínio é ortogonalidade : esta é a propriedade que os casos que governam os subsistemas permanecem independentes quando esses subsistemas são combinados. Nenhuma combinação dá origem a "casos especiais". Se um caso de quatro algarismos é combinado com um caso de três caracteres ortogonalmente, existem doze casos, mas idealmente cada caso é uma combinação de dois casos que permanecem independentes. De certo modo, não há realmente doze casos; as combinações são apenas "fenômenos emergentes de caso" com os quais não precisamos nos preocupar. O que isso significa é que ainda temos quatro casos em que podemos pensar sem considerar os outros três no outro subsistema e vice-versa. Se algumas das combinações tiverem que ser especialmente identificadas e dotadas de lógica adicional, então o raciocínio é mais difícil. Na pior das hipóteses, cada combinação tem algum tratamento especial e, em seguida, há realmente doze novos casos, além dos quatro e três originais.

    
por 21.06.2017 / 03:56
fonte
0

Claro. Tome a simultaneidade:

Seções críticas impostas por mutexes: fáceis de entender, porque há apenas um princípio (dois segmentos de execução não podem entrar na seção crítica simultaneamente), mas propensos à ineficiência e ao impasse.

Modelos alternativos, por ex. programação ou atores livres de bloqueios: potencialmente muito mais elegantes e poderosos, mas inferneamente difíceis de entender, porque você não pode mais confiar em conceitos (aparentemente) fundamentais como "agora escreva esse valor para aquele lugar" .

Ser fácil de raciocinar é um aspecto de um método. Mas escolher qual método usar requer considerar os aspectos todos em combinação.

    
por 20.06.2017 / 11:12
fonte
0

Vamos limitar a tarefa ao raciocínio formal. Porque o raciocínio humorístico, inventivo ou poético tem leis diferentes.

Mesmo assim, a expressão é pouco definida e não pode ser definida de maneira estrita. Mas isso não significa que deva permanecer tão obscuro para nós. Vamos imaginar que uma estrutura está passando por algum teste e recebendo marcas para diferentes pontos. As boas notas para TODOS os pontos significam que a estrutura é conveniente em todos os aspectos e, portanto, "Fácil de raciocinar".

A estrutura "Fácil de raciocinar" deve receber boas notas para o seguinte:

  • Os termos internos têm nomes razoáveis, facilmente distinguíveis e definidos. Se os elementos tiverem alguma hierarquia, a diferença entre os nomes pai e filho deve ser diferente da diferença entre os nomes dos irmãos.
  • Número de tipos de elementos estruturais é baixo
  • Os tipos usados de elementos estruturais são coisas fáceis com as quais estamos acostumados.
  • Os elementos dificilmente compreensíveis (recursões, meta-etapas, 4 + geometria dimensional ...) são isolados - não diretamente combinados entre si. (por exemplo, se você tentar pensar em alguma regra recursional mudando para cubos 1,2,3,4..n.dimensional, será muito complicado. Mas se você vai transferir cada uma dessas regras para alguma fórmula dependendo de n, você terá separadamente uma fórmula para cada n-cubo e separadamente uma regra de recursão para tal fórmula. E que duas estruturas separadamente podem ser facilmente pensadas)
  • Tipos de elementos estruturais são obviamente diferentes (por exemplo, não usar matrizes mistas a partir de 0 e de 1)

O teste é subjetivo? Sim, naturalmente é. Mas a expressão em si é subjetiva também. O que é fácil para uma pessoa, não é fácil para outra. Então, os testes devem ser diferentes para os diferentes domínios.

    
por 20.06.2017 / 16:31
fonte
0

A ideia de que as linguagens funcionais podem ser raciocinadas vem de sua história, especificamente ML que foi desenvolvido como uma linguagem de programação análoga às construções que a Lógica para Funções Computáveis usou para raciocinar. A maioria das linguagens funcionais está mais próxima dos cálculos de programação formal do que dos imperativos, de modo que a tradução do código para a entrada de um sistema de sistema de raciocínio é menos onerosa.

Para um exemplo de um sistema de raciocínio, em pi-calculus, cada localização de memória mutável em uma linguagem imperativa precisa ser representada como um processo paralelo separado, enquanto uma sequência de operações funcionais é um processo único. Quarenta anos depois do provador do teorema LFC, estamos trabalhando com GB de RAM, portanto, ter centenas de processos é um problema menor - usei pi-calculus para remover potenciais deadlocks de algumas centenas de linhas de C ++, apesar da representação ter centenas de processa o raciocinador esgotou o espaço de estado em cerca de 3GB e cura um bug intermitente. Isso teria sido impossível nos anos 70 ou exigiria um supercomputador no início dos anos 90, enquanto o espaço de estado de um programa de linguagem funcional de tamanho semelhante era pequeno o suficiente para raciocinar na época.

Das outras respostas, a frase está se tornando uma frase de efeito, embora muitas das dificuldades que dificultam raciocinar sobre linguagens imperativas sejam corroídas pela lei de Moore.

    
por 21.06.2017 / 23:27
fonte
-2

Fácil de raciocinar é um termo culturalmente específico, e é por isso que é tão difícil encontrar exemplos concretos. É um termo que está ancorado às pessoas que devem fazer o raciocínio.

"Fácil de raciocinar" é na verdade uma frase muito descritiva. Se alguém está olhando para o código e quer raciocinar sobre o que faz, é fácil =)

Ok, decompô-lo. Se você está olhando para o código, geralmente quer que ele faça alguma coisa. Você quer ter certeza de que faz o que você acha que deveria fazer. Então você desenvolve teorias sobre o que o código deve estar fazendo, e então raciocina sobre isso para tentar argumentar por que o código realmente funciona. Você tenta pensar sobre o código como um humano (e não como um computador) e tenta racionalizar argumentos sobre o que o código pode fazer.

O pior caso para "fácil de raciocinar" é quando a única maneira de fazer qualquer sentido do que o código faz é passar linha por linha pelo código como uma máquina de Turing para todas as entradas. Neste caso, a única maneira de raciocinar qualquer coisa sobre o código é se transformar em um computador e executá-lo em sua cabeça. Estes piores exemplos de casos são facilmente vistos em disputas de programação obsedadas, como estas 3 linhas de PERL que descriptografam RSA:

#!/bin/perl -sp0777i<X+d*lMLa^*lN%0]dsXx++lMlN/dsM0<j]dsj
$/=unpack('H*',$_);$_='echo 16dio\U$k"SK$/SM$n\EsN0p[lN*1
lK[d2%Sa2/d0$^Ixp"|dc';s/\W//g;$_=pack('H*',/((..)*)$/)

Quanto mais fácil de raciocinar, mais uma vez, o termo é altamente cultural. Você tem que considerar:

  • Quais habilidades o raciocinador tem? Quanta experiência?
  • Que tipo de perguntas o raciocinador pode ter sobre o código?
  • Quão certo o raciocinador precisa ser?

Cada um desses afeta "fácil de raciocinar" de forma diferente. Tome as habilidades do raciocinador como um exemplo. Quando eu comecei na minha empresa, foi recomendado que eu desenvolvesse meus scripts no MATLAB porque é "fácil de raciocinar". Por quê? Bem, todos na empresa conheciam o MATLAB. Se eu escolhesse uma língua diferente, seria mais difícil alguém me entender. Não importa que a legibilidade do MATLAB seja atroz para algumas tarefas, simplesmente porque ele não foi projetado para elas. Mais tarde, conforme minha carreira progredia, o Python se tornava cada vez mais popular. De repente, o código do MATLAB tornou-se "difícil de raciocinar" e Python foi a língua de preferência para escrever código que era fácil de raciocinar.

Considere também quais idoms o leitor pode ter. Se você puder confiar em seu leitor para reconhecer uma FFT em uma sintaxe específica, é "mais fácil raciocinar" sobre o código, se você mantiver essa sintaxe. Ele permite que eles olhem para o arquivo de texto como uma tela em que você pintou uma FFT, em vez de ter que entrar nos detalhes básicos. Se você estiver usando C ++, descubra o quanto seus leitores estão confortáveis com a biblioteca std . Quanto eles gostam de programação funcional? Alguns dos idiomas que saem das bibliotecas de containers são muito dependentes de qual estilo idiomático você prefere.

É também importante entender em que tipo de perguntas o leitor pode estar interessado em responder. Seus leitores estão mais preocupados com a compreensão superficial do código, ou estão procurando por insetos nas entranhas?

O quão certo o leitor tem que ser é realmente interessante. Em muitos casos, o raciocínio nebuloso é realmente suficiente para levar o produto para fora da porta. Em outros casos, como o software de voo da FAA, o leitor vai querer ter raciocínio ironclad. Eu me deparei com um caso em que argumentei o uso de RAII para uma tarefa em particular, porque "Você pode apenas configurá-lo e esquecê-lo ... ele fará a coisa certa". Disseram-me que eu estava errado sobre isso. Aqueles que estavam indo para raciocinar sobre este código não eram o tipo de pessoas que "só querem esquecer os detalhes". Para eles, RAII era mais como um chad pendurado, forçando-os a pensar em todas as coisas que podem acontecer quando você sai do escopo. Aqueles que estavam lendo esse código realmente preferiam chamadas de função explícitas no final do escopo, para que pudessem ter certeza de que o programador pensou sobre isso.

    
por 20.06.2017 / 19:54
fonte