Pensar em termos de algoritmos é útil para as diversas atividades cotidianas, seja cozinhar, procurar um número telefônico, agendar um compromisso, fazer compras. Mas queremos escrever algoritmos principalmente para utilizar um computador. Por isso, precisamos entender como fazer com que o algoritmo saia do papel e passe a ser executado pelo computador.
Lembre-se de que dissemos que os computadores são máquinas burras que só são capazes de realizar tarefas bem simples, como inverter ou zerar bits na memória. Esse conjunto de instruções que o computador é capaz de realizar é o que chamamos de linguagem de máquina. Um algoritmo escrito em linguagem de máquina, por sua vez, é uma sequência de bits e bytes. Para que possamos visualizar um algoritmo em linguagem de máquina, normalmente escrevemos uma representação em texto, em uma linguagem de montagem, ou assembly language. Por exemplo, um trecho de código em assembly se parece com
LOOP: MOV A, 3 INC A JMP LOOP
Pode parecer surpreendente que essas máquinas realizem tarefas tão elaboradas, como simulações químicas, controle de tráfego aéreo, etc. O que permite que consigamos instruir os computadores a executar tarefas tão elaboradas, mesmo que eles só realizem tarefas bem simples é que nós escrevemos os algoritmos para os computadores em um idioma mais abstrato do que as linguagens de máquina.
Mas quando escrevemos um algoritmo, digamos, em português, estamos escrevendo para uma outra pessoa. Não há computador que genuinamente entenda o que significa "percorrer uma lista", "adicionar ao número anotado", etc. Que entenda, por exemplo, "percorra a lista somando os valores". A principal dificuldade aqui é que o português é uma linguagem ambígua — e o computador não pode adivinhar qual o significado de cada uma dessas instruções.
Para escrever um algoritmo de forma que o computador entenda, usamos uma linguagem de programação. Assim, depois de escrito o algoritmo (normalmente em português ou em algum formato parecido), uma pessoa, a programadora traduz esse algoritmo para uma linguagem de programação. Assim, ela transforma um algoritmo em um programa correspondente. Observe que a programadora pode, inclusive, não ser a mesma pessoa que criou o algoritmo.
Uma linguagem que é inambígua para o computador é a própria linguagem de máquina, ou a linguagem de montagem correspondente. Mas isso não deixaria feliz a programadora, que teria que escrever programas imensos, mesmo para tarefas bem simples. Por isso, utilizamos uma linguagem de programação de alto nível, para que escrevamos programas mais parecidos com o algoritmo que nós normalmente escreveríamos, mas que seja inambíguo como exige um computador.
Sintaxe
Uma linguagem de programação normalmente tem uma sintaxe rígida, que é o conjunto de regras que determina quais combinações de símbolos e palavras-chaves podem ser utilizadas. Por exemplo, se em uma linguagem a palavra-chave para ler um número e guardá-lo na variável X for input X, escrever read X iria resultar em um erro de sintaxe. Por mais que uma pessoa entenderia qual o objetivo da programadora ao escrever a instrução errada, um computador não entenderia. A razão para escolher uma palavra input em inglês é para facilitar a leitura, mas uma palavra-chave poderia ser qualquer sequência de símbolos.
Considere o programa a seguir um uma linguagem hipotética.
input N ; X := 0 ; for Y from 1 to N do X := X + Y end ; output X
Nessa linguagem, o símbolo :=
representa uma atribuição, assim
escrevemos X := 0
, quando normalmente escreveríamos X $\gets$ 0 nos
nossos algoritmos anteriores. A linguagem pode ter várias estruturas
de controle, como a estrutura que começa com for
, que realiza um
conjunto de instruções um determinado número de vezes.
Além disso, a ordem com que os símbolos e palavras chaves aparecem é de fundamental importância. Essa ordem é determinada pela sintaxe. Nessa linguagem hipotética retirada da bibliografia, a sintaxe é definida pelo seguinte diagrama
Repare que depois de for
esperamos uma variável, depois a
palavra-chave for
e assim por diante. Portanto, se tivéssemos escrito
input N ; X := 0 ; for Y to N from 1 do X := X + Y end ; output X
o computador se recusaria a continuar, acusando um erro de sintaxe! Existem várias outras maneiras de representar a sintaxe de uma linguagem de programação, mas não precisamos delas agora.
A maioria das linguagens utilizam apenas símbolos e palavras-chaves
para determinar sua sintaxe. Assim, o conjunto de instruções que
correspondem a uma iteração do for
acima é terminado com a
palavra-chave end
. Algumas linguagens, no entanto, utilizam outros
mecanismos. Por exemplo, um código em Python correspondente seria
N = int(input()) X = 0 for Y in range(1, N + 1): X = X + Y print(X)
Enquanto esses algoritmos são equivalentes, a sintaxe das linguagens
de programação diferentes levaram a programas muito diferentes.
Observe que o conjunto de instruções de uma iteração do for
nesse
caso é identificado por meio do recuo (ou indentação). Além disso,
utilizamos um símbolo diferente para representar atribuição: ao invés
de :=
, utilizamos =
. A escrever ou ler um programa, é
imprescindível atentar-se à sintaxe da linguagem de programação!
Semântica
Além da sintaxe, uma linguagem de programação deve definir uma semântica inambígua, isso é, a linguagem de programação deve definir o que significa cada uma das frases permitidas. Se a sintaxe não tivesse acompanhada de semântica, então o segmento
for Y from 1 to N do
poderia muito bem significar "subtraia Y de 1 e armazene o resultado
em N". Nada garante, por exemplo, que as palavras for
, to
e from
tenham o mesmo significado que em inglês. Quem garante que que :=
significa atribuição, ou mesmo que +
é o operador de soma? É
a semântica que determina o que significa cada frase escrita
em uma linguagem de programação.
Mais do que isso, a semântica deve ser precisa e completa. Mesmo que
as palavras-chaves tenham o significado usual em inglês, pode haver
instruções que são ambíguas ou indefinidas. Por exemplo, se N é um
número inteiro positivo 10, então é razoável presumir que a iteração
do for
acima irá executar 10 vezes, com valores de Y = 1, 2, ...,
10. Mas se N for -314.1592? O corpo do laço não deve ser executado,
ou será que ele deve ser executado para valores 1, 0, −1, −2, ...,
-313, and -314?
Enquanto a definição do que é um programa válido sintaticamente é normalmente fácil e, em geral, definido por gramáticas precisas e outros tipos de especificação, como os diagramas acima, definir a semântica de uma linguagem de programação não é uma tarefa trivial. Ela é normalmente definida na documentação fornecida, normalmente nos manuais de linguagem, que são compostos por capítulos e capítulos de descrição. Pode ser surpreendente descobrir que mesmo linguagens de programação modernas ainda não tenham uma semântica completamente definida.
Compilação
Uma vez que temos um algoritmo escrito em uma linguagem de programação, ainda precisamos de um processo chamado de compilação, que é responsável por converter nosso programa de uma linguagem de programação de alto nível para a linguagem de montagem. Já falamos um pouco antes sobre a linguagem de montagem; essa linguagem contém instruções muito simples e análogas à linguagem de máquina. Ela contém instruções como ler ou armazenar uma palavra na memória, fazer operações aritméticas, desviar o fluxo de execução (goto ou if-then) etc.
Por exemplo, um programa típico
for Y from 1 to N do {corpo do laço} end
seria traduzido um código em linguagem de montagem que se parece com
MVC 0, Y # guarda a constante 0 na posição Y LOOP: CMP N , Y # compara os valores nas posições N e Y JEQ REST # se forem iguais, pula à instrução rotulada REST ADC 1, Y # adiciona a constante 1 ao valor na posição Y # ... corpo do laço traduzido ... JMP LOOP # volta à instrução rotulada LOOP REST: # ... restante do programa ...
Essa tarefa de tradução de uma linguagem de programação de alto nível para uma linguagem de programação de baixo nível é realizada por um programa bem sofisticado chamado compilador. O compilador é normalmente fornecido pelo desenvolvedor da linguagem ou pelo vendedor do hardware e deve ser específico para cada processador. Depois de obtido um programa em linguagem de montagem, ainda é necessário convertê-lo em linguagem de máquina. Isso normalmente é feito automaticamente pelo compilador, que invoca uma série de programas de sistemas (o chamado system software), como montadores, carregadores etc.
Esses programas de sistema têm uma série de funções, com o papel de facilitar um conjunto de modos de operação de alto nível do computador. Isso permite isolar o usuários de vários detalhes de baixo nível envolvidos. Um dos principais programas de sistema é o chamado sistema operacional, que é responsável por gerenciar recursos e periféricos, fornecendo ao programa de usuário nesses modos de operação de alto nível. Simplificadamente, podemos então entender a execução de um programa em camadas, em que camadas mais acima têm abstrações de alto nível, enquanto camadas mais abaixo tem abstrações mais próximas do hardware.
Programas de Aplicação |
Compiladores |
Sistema operacional |
Hardware |
Interpretação e execução imediata
Existe ainda uma outra maneira de executar programas escritos em uma determinada linguagem de programação. Ao invés de converter o código-fonte do programa em uma linguagem de máquina, para depois executar o programa traduzido, podemos executar cada instrução da linguagem de programação de alto nível assim que ela for reconhecida. Assim, ao invés de usarmos um compilador, usamos um programa que é responsável por essa tradução local e execução imediada, chamado de interpretador.
A forma com que cada interpretador é criado pode variar bastante de implementação para implementação. Em muitos casos, você pode imaginar simplesmente que o interpretador realiza todo o processo de compilação e executa o programa obtido diretamente, sem a necessidade de invocá-lo. Algumas vezes, no entanto, as instruções são executadas à medida em que as escrevemos. Isso acontece, por exemplo, quando utilizamos o modo interativo de linguagens de programação de script, como Python, Ruby, etc.
Existem algumas razões para se criar um interpretador ao invés de um compilador, entre as quais:
-
normalmente é mais fácil escrever um interpretador para uma linguagem de programação, do que um compilador completo;
-
é mais fácil entender o que está acontecendo durante a execução do programa em linguagens interpretadas, particularmente quando trabalhando interativamente através de um um terminal conectado a tela do computador.
Do problema à execução
Desde o conhecimento do problema até a execução do programa no computador para obter uma solução, existe um processo que passa por diversas etapas. Por isso, é importante entender bem esse processo e realizar bem cada uma das etapas, sem tentar pular passos. Assim, faça sempre o seguinte:
-
Entenda o problema! Descreva qual é o conjunto de entradas válidas e qual é o conjunto de saídas. Procure especificar também qual saída deve corresponder a cada entrada válida.
-
Estude o problema e tente resolver algumas instâncias desse problema. Tome nota de que passos você realiza. Com isso, você terá uma ideia de como resolver uma instância genérica do problema.
-
Tente expressar suas ideias na forma de um algoritmo. Lembre-se de sempre de utilizar apenas instruções elementares. Também, sempre teste seu algoritmo para instâncias pequenas, até se convencer de que ele está correto.
-
Apenas depois de ter escrito e testado seu pequeno algoritmo, reescreva-o em uma linguagem de programação.
-
Você irá compilar o código-fonte e executar o programa criado, ou irá interpretar o código-fonte diretamente, dependendo da linguagem. Você pode encontrar uma série de erros:
-
Muitas vezes, haverá erros de sintaxe e você precisará corrigir seu programa para que ele seja um texto válido.
-
Outras vezes, embora o programa seja válido sintaticamente, ocorrerão erros. Isso pode acontecer por uma série de motivos. Esses são chamados erros de execução. Você precisará revisar seu código-fonte e voltar ao passo 4.
-
Finalmente, pode ser que você não encontre nenhum erro, mas os resultados obtidos pelo seu programa não estejam corretos. Esses são os chamados erros de lógica. Isso pode acontecer porque você escreveu um código-fonte que não corresponde ao seu algoritmo, ou porque seu algoritmo está incorreto. Nesse caso, será preciso voltar ao passo 3.
-
A figura extraída do livro representa o processo de compilação e execução.