Como desenvolvedores de software usamos a lógica a todo o momento. Contudo, talvez não sejamos conscientes de como a usamos e que de fato existe mais de um tipo de lógica que precisamos dominar.
Para entendermos melhor as diferenças entre os diferentes tipos de raciocínio lógico e como eles se relacionam ao desenvolvimento precisamos falar de algum formalismo lógico.
Uma premissa é uma informação que conhecemos ou assumimos como verdadeira. Pode ser um facto real ( o céu é azul) ou apenas verdadeira num certo contexto que interessa ao problema ( b < a). A conclusão é uma nova informação que é derivada de uma ou mais premissas através de regras.
Repare que a regra que transforma as premissas em conclusão pode ser dos mais variados contextos. Pode ser uma simples operação aritmética entre números ou entre valores de verdade (Álgebra de Boole). Pode ser um silogismo, ou pode algo mais especifico ao domínio da aplicação sendo desenvolvida.
O raciocínio lógico que frequentemente associamos com lógica de programação é o raciocínio dedutivo, ou, Dedução. Dadas certas premissas e regras usamos a Dedução para obter a conclusão. Em termos simples, apenas operamos sobre as premissas com as regras e obtemos a resposta. Se a regra é por exemplo uma tabela de verdade OR e as premissas são a é VERDADEIRO
e b é FALSO
então podemos deduzir, pela regra da tabela de verdade da operação OR, que a OR b é VERDADEIRO
.
A dedução é o primeiro tipo de raciocínio que desenvolvemos, e podemos dizer que é a ferramenta mais básica do desenvolvedor. O raciocínio dedutivo é mais próximo do raciocínio matemático e é o foco nos primeiros anos de contato com programação em que o programador se concentra em obter os dados de entrada, operar sobre eles com alguma regra e obter uma conclusão que serve como dado de saída.
A Indução, ou raciocínio indutivo serve para determinar a regra. Aqui sabemos as premissas e a conclusão, e o objetivo é determinar a regra.
Este é um processo mais complexo que a Dedução. Mais do que uma regra pode levar as mesmas premissas à conclusão.
A indução é muito usada em ciências. Em ciências conseguimos observar as premissa e as conclusões, mas estamos achando o que as conecta.
A Indução também pode servir para determinar se uma regras é válida para todos os elementos de um conjunto.
Como desenvolvedores usamos a Indução para criar as regras das nossas funções e métodos. Sabemos os inputs e outputs esperados e derivamos a regra que transforma um no outro. Também utilizamos este tipo de raciocino quando construímos loop de repetição como sejam while
e for
.
Se a Dedução é condição para começar a programar, a Indução é essencial para ser um bom programador já que é muito improvável poder construir código sem loops ou sem funções.
A indução também é usada no processo de levantamento de requisitos em que é necessário descobrir as regras do sistema. Algumas serão dadas, mas alguma irão aparecer pela meticulosa analise das entradas e saídas.
Principio da Parcimónia (Navalha de Occam)
O Principio da Parcimónia, também conhecido como Navalha de Occam (Occam’s Razor) é um principio usado para escolher entre diferentes resultados da indução. Se durante o processo de indução obtemos uma ou mais regras possíveis que transformam as mesmas premissas nas mesmas conclusões deveremos escolher a que necessita de menos premissas extra.
Muitas vezes os programadores menos experientes criam funções com muitos passos , muitos ramos de decisão (ifs) enquanto um programador mais experiente escreve a função que faz o mesmo com muito menos passos e ifs. Esses “ifs” a mais são premissas extra, que muitas vezes não são necessárias. Portanto, a diferença entre eles não é a experiência. É que um está usando o principio da parcimônia e o outro não.
Inteligência Artificial
A área de Inteligência Artificial (IA) é basicamente uma forma de driblar o processo de Indução. Construímos um software que de alguma forma contém a regra, mas não podemos realmente saber qual é.
Isto é especialmente claro em processo de IA que usam treinamento. A ideia é que temos um software com diversos parâmetros. Ao manipular esses parâmetros conseguimos obter a resposta certa para todos os dados. Mas não sabemos quais seriam os seu valores. Então determinamos os seus valores a partir de valores de entrada (premissas) e saída (conclusões) que já sabemos à priori. Este processo gera parâmetros cada vez mais eficientes de forma que se dermos ao sistema uma premissa nova – um dado que ele nunca viu – o sistema saberá dar uma resposta – a conclusão – induzida pelos parâmetros obtidos no treinamento.
O problema destes mecanismo sde IA é o processo de treinamento em si. Se as premissas e conclusões não são boas amostragens do universo, o treinamento será enviesado (screwed) e futuros resultados não serão correspondentes com o universo amostrado.
Portanto, a Indução é uma atividade um tanto frágil pois não podemos confiar que a regra que obtivemos é realmente universal. Por isso precisamos de testes experimentais.
Indução e Testes
Como falei antes, a Indução é muito usada em Ciência. Mas por causa da deficiência em encontrar regras únicas e universais temos que lançar mão de mais algumas ferramentas. Já vimos o Principio da Parcimónia para reduzir o número de opções, mas temos que olhar também os testes experimentais.
Os testes visam dois objetivos: 1) provar que a regra realmente é compatível com as premissas e conclusões já conhecidas. 2) provar que novas premissas e conclusões se submetem à mesma regra.
Em desenvolvimento podemos considerar testes unitários como resolvendo o primeiro ponto e testes exploratórios resolvendo o segundo.
Podemos imaginar uma função de soma de dois doubles. E construir alguns testes unitários para verificar que realmente 1+1
são 2
e 2+2
são 4
, etc… Mas se não testarmos com valores como null
, NaN
, zero negativo, infinitos números muito pequenos e muito grandes não estamos realmente sabendo se a função realmente soma dois números corretamente. Até mesmos um relógio parado está certo duas vezes por dia.
Este talvez seja o menos conhecido, e portanto menos usado, raciocínio lógico. Aqui estamos perante regras e conclusões, e queremos encontrar as premissas. Este é um método usado pelos detectives que sabem o crime cometido ( a conclusão) , podem saber o como aconteceu (as regras) , mas querem saber quem o cometeu e por quê ( as premissas). Este é o método mais utilizado pelo famoso personagem detective Sherlock Holmes nas novelas de Arthur Conan Doyle.
Em desenvolvimento este tipo de raciocínio lógico é muito útil quando queremos descobrir a causa de bugs. Sabemos a conclusão – o sistema apresenta erro – sabemos o código que escrevemos – as regras- mas não sabemos das causas.
Fazer debuging do sistema pode ser feito percorrendo todas as linhas de código e vendo como os dados são operados para ver onde o código faz algo que não deveria. Esta é uma forma observacional de debuging que depende de ferramentas que permitem observar o código e o estado do sistema enquanto o código executa. Nem sempre esta forma é possível se ser usada. Especialmente em sistemas altamente paralelizados.
Às vezes temos que raciocinar logicamente usando Abdução para encontrar o problema, pois não temos as ferramentas ou nem sequer é possível as usarmos no ambiente onde o erro ocorre. Quando o erro só ocorre em produção, por exemplo.
Boas mensagens de erro e bom tratamento de exceções ajudam bastante por que quando são bem feitos trazem informação útil para descobrir mais sobre a origem do problema.
Ser consciente do tipo de raciocínio que estamos usando e se ele se adequa ao que queremos alcançar facilita o processo de raciocínio. Para ser um bom programador não basta chegar numa conclusão, é preciso muitas vezes desvendar a regra – o algoritmo – por detrás e às vezes até as causas.
Alcançar um bom domínio das três formas de raciocínio pode vir com a prática, mas você pode usar alguns atalhos se for consciente de qual tipo precisa usar.
Não é uma questão de experiência. É uma questão de treinamento. E você pode treinar todos os dias, mesmo em situações que não são de trabalho.