Estrutura e Interpretação de Programas de Computadores

Anotações dos meus estudos

Conteúdo

Recursos

  • Fonte: PDF gratuito do MIT
  • Exemplos: MIT/GNU Scheme
  • Ainda estou decidindo se uso o termo procedure no original, em inglês.

Introdução

Ciência da computação hoje

  • Gestão de projetos e empreendimentos
  • Desenvolvimento de softwares aplicativos
  • Gestão e manipulação de dados
  • Projetos de redes e sistemas distribuídos
  • Percepção das oportunidades de automação de processos

Concepção no MIT 6.001

A ciência da computação não é simplesmente sobre computadores, nem deve ser confundida com os instrumentos utilizados na prática. Em vez disso, a ciência da computação é um campo de estudo que se concentra em entender e formalizar processos e estruturas de informação.

Ciência da computação tem a ver com:

  • Formalizar intuições sobre processos.
  • Descobrir como fazer as coisas.
  • Desenvolver um jeito de falar com precisão sobre “como conhecer”.

Os problemas computacionais podem ser enfrentados com as seguintes habilidades e ferramentas:

  • Pensamento crítico e computacional
  • Abstração e modularidade
  • Recursão e eficiência de processos recursivos
  • Interpretação de linguagens de programação
  • Metalinguística

Saberes declarativos e imperativos

Saber declarativo (qual é a verdade)

Refere-se ao conhecimento sobre fatos e informações, que são descritos e conhecidos por meio de declarações ou proposições. Esse tipo de conhecimento é associado à compreensão e memorização de informações, teorias, e conceitos.

Em ciência da computação, é o conhecimento sobre a sintaxe e semântica de uma linguagem de programação, regras de algoritmos, e definições de conceitos como "árvore binária" ou "estrutura de dados".

Características:

  • Conhecimento de Fatos: saber o que algo é, quais são suas propriedades e como se relaciona com outras coisas.
  • Memorável: pode ser ensinado e avaliado por meio de testes que verificam a capacidade de recordar e explicar informações.

Exemplo:

A raiz quadrada de X é o número maior ou igual a zero que, elevado ao quadrado, resulta em X.

Isso não nos diz nada sobre como encontrar esse número.

Saber imperativo (como chegar à verdade)

Refere-se ao conhecimento sobre como realizar tarefas ou como fazer algo. Esse tipo de conhecimento está associado à prática e à execução de ações, processos e procedimentos.

Em ciência da computação, é saber como implementar um algoritmo específico, escrever código que executa uma tarefa, ou configurar um ambiente de desenvolvimento. Envolve habilidades práticas, como a programação, a construção de sistemas e a resolução de problemas técnicos.

Características:

  • Conhecimento de Processos: saber como fazer algo ou como realizar uma tarefa.
  • Prático: pode ser ensinado e avaliado por meio de exercícios práticos, projetos e tarefas que exigem a aplicação de habilidades.

Exemplo:

Para chegar à raiz quadrada aproximada de X, infira um valor Y; melhore o valor arbitrado a partir da média entre Y e X/Y; continue aproximando Y até que Y*Y seja o mais próximo possível de X.

– Método de Herão de Alexandria

O que é um processo

Você pode pensar em processos como espíritos mágicos que vivem nos computadores e fazem alguma coisa: e o que dirige um processo é um conjunto de regras chamado de “procedimento”. Procedimentos são feitiços que controlam os espíritos mágicos, que são os processos. […] Nós vamos conjurar nossos espíritos numa linguagem mágica chamada Lisp.

– Harold Abelson, na palestra 1A, introdutória ao MIT 6.001.

Portanto, segundo esta metáfora:

  • Processos descrevem como algo será feito;
  • Procedimentos descrevem as instruções para a realização de um processo;
  • Procedimentos são escritos com linguagens de programação para descrever processos.

Por quê Lisp?

Fácil de aprender (poucas regras e sintaxe uniforme)
Lisp e Scheme têm uma sintaxe minimalista e uniforme, com um conjunto reduzido de regras e uma abordagem consistente para a expressão em código.
Não rouba a atenção do que realmente importa
São linguagens que destacam os conceitos de programação e as abstrações subjacentes, sem adicionar complexidade desnecessária, o que ajuda a manter o foco no entendimento de conceitos como funções, recursão, e abstração.
O que importa são as implicações das regras da linguagem
Possibilitam observar como as regras da linguagem afetam o comportamento e a estrutura dos programas.
Explorar as regras da linguagem para dominar a programação
Tendo liberdade para explorar, é possível desenvolver habilidades para pensar criticamente sobre como aplicar e modificar as regras da linguagem para resolver problemas complexos e desenvolver novas abordagens.

Controlando complexidades

O alvo da ciência da computação não está nas regras da linguagem, mas nas implicações dessas regras; está em formalizar o saber imperativo, o “como fazer as coias”. Isso não é o mesmo que dizer às pessoas como obter a raiz quadrada de um número: a verdadeira questão é como construir sistemas muito grandes, programas com milhares de páginas, programas que ninguém, sozinho, seria capaz de assimilar de uma vez.

Isso só é possível porque existem técnicas para controlar a complexidade de sistemas muito grandes – e a ciência da computação é sobre descobrir essas técnicas. Contudo, diferente das complexidades de outras áreas, como a engenharia, a ciência da computação lida com componentes idealizados. Isso quer dizer que, em certos aspectos, há pouca diferença entre o que pode ser feito e o que pode ser imaginado: as limitações não são físicas, elas estão em nossas próprias mentes, no até onde conseguimos imaginar.

As técnicas para controlar complexidades não são exclusivas da ciência da computação.

Abstração da caixa preta

Basicamente, significa esconder as complexidades desnecessárias ou que já possuam um processo de solução satisfatório definido.

            Caixa preta
        +----------------------+
144 --->|  Raiz quadrada de X  |---> 12
        +----------------------+

Deste modo, operações mais complexas também poderão ser abstraídas:

        +----------------------+       +---+
144 --->|  Raiz quadrada de X  |------>|   |
        +----------------------+       |   |
                                       | + |---> 18
        +----------------------+       |   |
 36 --->|  Raiz quadrada de X  |------>|   |
        +----------------------+       +---+

Para quem tem interesse no resultado da operação, não importa o que está nas caixas pretas.

Uma implicação da caixa preta, é que ela possibilita generalizações. Por exemplo, o método de Hirão para calcular a raiz quadrada aproximada de X traz, em si mesmo, um saber imperativo ainda mais geral que é a definição do ponto fixo de uma função:

Ponto fixo da função

Para encontrar o valor de Y em que f(Y) = Y (ponto fixo), infira um valor para Y; continue aproximando este valor na função f(Y) até que o resultado seja o mais próximo possível de Y.

Exemplo: para computar a raiz quadrada de X, encontre o ponto fixo da função f(Y), que é a média entre Y e X/Y. Quando Y for igual à média entre Y e X/Y, nós teremos a raiz quadrada de X.

Sendo assim, o saber imperativo da computação da raiz quadrada é um caso especial de um saber imperativo mais geral, que é a computação do ponto fixo de uma função.

                         +--------------+      +-----------------+
f(Y) = média(Y, X/Y) --->|  Ponto Fixo  |  =>  |  Raiz quadrada  |
                         +--------------+      +-----------------+

Portanto, além de criar caixas para a computação direta de valores numéricos, nós também podemos criar caixas para computar métodos. Com isso, nós temos um procedimento cujo valor é outro procedimento: isso importa porque os procedimentos são os meios pelos quais a ciência da computação fala sobre saberes imperativos.

A abstração da caixa preta é uma ferramenta para elaborar a compreensão de vários aspectos de uma linguagem de programação, em especial:

Seus objetos primitivos
  • Procedimentos primitivos
  • Dados primitivos
Seus meios de combinação
  • Composição de procedimentos
  • Construção de dados compostos
Seus meios de abstração
  • Definição de procedimentos
  • Abstração de dados simples
Suas formas de expressar padrões comuns
  • Procedimentos de alta ordem (procedimentos como dados)
  • Dados como procedimentos

Convenções de interfaces

Abstração metalinguística

Construindo abstrações com procedimentos

Uma linguagem de programação poderosa é mais do que apenas um meio para instruir um computador a realizar tarefas. A linguagem também serve como um framework no qual nós organizamos nossas ideias a respeito de processos.

– SICP: 1.1 The Elements of Programming (2nd edition, pag. 6)

Toda linguagem poderosa tem três mecanismos para formar ideias complexas a partir de ideias simples:

Expressões primitivas
Representam as entidades mais simples com que a linguagem se preocupa.
Meios de combinação
Meios pelos quais elementos compostos podem ser formados a partir de elementos mais simples.
Meios de abstração
Meios pelos quais os elementos compostos podem ser nomeados e manipulados como unidades.

Em programação, nós lidamos com dois tipos de elementos:

Dados
Aquilo que deverá ser manipulado.
Procedimentos
Descrição das regras para a manipulação dos dados.

Mais adiante, nós descobriremos que esses elementos não são tão distintos assim.

Portanto, qualquer linguagem poderosa tem que ser capaz de descrever dados e procedimentos primitivos e deve oferecer métodos para combiná-los e abstraí-los.

Expressões

Expressões são elementos que expressam um valor. Por exemplo, nós podemos abrir o REPL do Scheme e digitar uma cadeia de dígitos 4 e 2, formando o que nós identificamos como o número 42.

1 ]=> 42

Ao teclamos Enter, o interpretador Scheme responderá:

;Value: 42

Expressões simples, como as que representam números, podem ser combinadas com expressões que representam procedimentos primitivos (como + ou *) para formar expressões compostas que representam a aplicação de um procedimento a um conjunto de dados. Por exemplo:

1 ]=> (+ 17 25)

;Value: 42

1 ]=> (- 1000 334)

;Value: 666

1 ]=> (* 30 12)

;Value: 360

1 ]=> (/ 126 3)

;Value: 42

Expressões como essas, formada por uma lista de expressões delimitadas por parêntesis para representar a aplicação de um procedimento, são chamadas de combinações. Nelas, os elementos mais à esquerda são chamados de operadores, enquanto os demais são chamados de operandos.

O valor de uma combinação é determinado pela aplicação do procedimento, especificado pelo operador, aos argumentos, que são os valores dos operandos.

Notação prefixada

A convenção de posicionar o operador antes dos operandos é conhecida como notação prefixada, o que pode trazer alguns benefícios. Um deles é a possibilidade de acomodar procedimentos que podem receber um número indeterminado de argumentos:

1 ]=> (+ 21 35 13)

;Value: 69

1 ]=> (* 2 2 2 2 2 2 2 2)

;Value: 256

Não existe ambiguidade, porque o operador sempre será o elemento mais à esquerda e toda a combinação é delimitada pelos parêntesis.

Uma segunda vantagem, é que a notação prefixada possibilita o aninhamento de combinações, ou seja, combinações cujos elementos também podem ser combinações:

1 ]=> (/ (+ 62 (* 16 4)) 3)

;Value: 42

A confusão é toda nossa

Em princípio, o interpretador Scheme não tem um limite para o aninhamento de combinações: no fim das contas, somo nós, humanos, que tendemos a ficar confusos com expressões que, para o interpretador, ainda são relativamente simples.

Um pouco dessa confusão pode ser mitigada com uma convenção de formatação chamada de pretty-printing (impressão bonita), que alinha verticalmente os operandos conforme os seus níveis de aninhamento:

(+ (* 3 (+ (* 2 4) (+ 3 5))) (+ (- 10 7) 6))

(+ (* 3
      (+ (* 2 4)
         (+ 3 5)))
   (+ (- 10 7)
      6))

Denominações e o ambiente

Um aspecto crítico das linguagens, são os meios que elas oferecem para que possamos utilizar nomes para fazer referência a objetos computacionais. Por exemplo, nós podemos definir um nome para identificar uma variável, cujo valor será o objeto computacional em questão.

No Scheme, isso é feito com o operador define:

1 ]=> (define number 123)

;Value: number

Isso faz com que o interpretador associe o valor 123 ao nome number. Assim que a associação é feita, nós podemos nos referir ao valor 123 pelo seu nome:

1 ]=> number

;Value: 123

O define é o meio de abstração mais simples do Scheme, pois possibilita a atribuição de nomes simples (circ) aos resultados de operações compostas:

1 ]=> (define pi 3.14159)

;Value: pi

1 ]=> (define raio 10)

;Value: raio

1 ]=> (define circ (* 2 pi raio))

;Value: circ

1 ]=> circ

;Value: 62.8318

Obviamente, para acompanhar o que acontece com os vários pares de nomes e objetos, o interpretador deve manter algum tipo de memória: essa memória é chamada de ambiente global.

A especificação do tipo de ambiente é importante, porque a computação envolve vários tipos de ambientes, inclusive o vetor ambiente, dos processos de sistemas UNIX e GNU/Linux.

Avaliando combinações

Digamos que, para avaliar combinações, o interpretador tenha que seguir o procedimento abaixo:

  1. Avalie as sub expressões da combinação.
  2. Aplique o procedimento com o valor da sub expressão mais à esquerda (o operador) aos argumentos com os valores das outras sub expressões (operandos).

Observe que o primeiro passo dita que, para realizar o processo de avaliação de uma combinação, antes, é preciso realizar o processo de avaliação de cada elemento da combinação. Portanto, a regra da avaliação é, por natureza, recursiva: ou seja, ela inclui, como um de seus passos, a necessidade de invocar a si mesma.

Por exemplo, para avaliar esta operação…

(* (+ 2 (* 4 6)) (+ 3 5 7))

A regra da avaliação terá que ser aplicada a quatro combinações diferentes, o que pode ser melhor observado numa representação em árvore:

eval-tree.png
Figure 1: Árvore representando os valores de cada sub combinação.