







































































Estude fácil! Tem muito documento disponível na Docsity
Ganhe pontos ajudando outros esrudantes ou compre um plano Premium
Prepare-se para as provas
Estude fácil! Tem muito documento disponível na Docsity
Prepare-se para as provas com trabalhos de outros alunos como você, aqui na Docsity
Encontra documentos específicos para os exames da tua universidade
Prepare-se com as videoaulas e exercícios resolvidos criados a partir da grade da sua Universidade
Responda perguntas de provas passadas e avalie sua preparação.
Ganhe pontos para baixar
Ganhe pontos ajudando outros esrudantes ou compre um plano Premium
Um apostila sobre a linguagem Lisp
Tipologia: Notas de aula
1 / 79
Esta página não é visível na pré-visualização
Não perca as partes importantes!








































































Em linguagens Funcionais toda a programação é realizada por meio da aplicação de funções a argumentos. As variáveis e as operações de atribuição usadas pelas linguagens imperativas não são necessárias. Os laços de repetição são substituídos por chamadas a funções recursivas. Baseiam-se fortemente nos conceitos das funções matemáticas.
Qualquer linguagem de programação lida com duas espécies de objetos: dados e procedimentos. Os dados são os objectos que pretendemos manipular. Os procedimentos são descrições das regras para manipular esses dados.
Se considerarmos a linguagem da matemática, podemos identificar os números como dados e as operações algébricas como procedimentos e podemos combinar os números entre si usando aquelas operações. Por exemplo, 2 × 2 é uma combinação, tal como 2 × 2 × 2 e 2 × 2 × 2 × 2. No entanto, a menos que pretendamos ficar eternamente a resolver problemas de aritmética elementar, somos obrigados a considerar operações mais elaboradas que representam padrões de cálculos. Neste último exemplo, é evidente que o padrão que está a emergir é o da operação de potenciação, i.e, multiplicação sucessiva, tendo esta operação sido definida na matemática há já muito tempo.
Tal como a linguagem da matemática, uma linguagem de programação deve possuir dados e procedimentos primitivos, deve ser capaz de combinar quer os dados quer os procedimentos para produzir dados e procedimentos mais complexos e deve ser capaz de abstrair padrões de cálculo de modo a permitir tratá-los como operações simples, definindo novas operações que representem esses padrões de cálculo.
1.1 - Elementos primitivos
Elementos primitivos são as entidades mais simples com que a linguagem lida. Um número, por exemplo, é um dado primitivo.
Como dissemos anteriormente, o Lisp executa um ciclo read-eval-print. Isto implica que tudo o que escrevemos no Lisp tem de ser avaliado, i.e., tem de ter um valor, valor esse que o Lisp escreve no ecran.
Assim, se dermos um número ao avaliador, ele devolve-nos o valor desse número. Quanto vale um número? O melhor que podemos dizer é que ele vale aquilo que vale. Por exemplo, o número 1 vale 1.
1 1 12345 12345
Como se vê no exemplo, em Lisp, os números podem ser inteiros ou reais.
Exercício 2
Descubra qual é o maior real que o seu Lisp aceita. Consegue fazer o mesmo para os inteiros?
1.2 - Combinações
Combinações são entidades complexas feitas a partir de entidades mais simples. Por exemplo, na matemáticas números podem ser combinados usando operações como a soma ou o produto. Como exemplo de combinações matemáticas, temos 1 + 2 e 1 + 2 × 3. A soma e o produto de números são exemplos de operações extremamente elementares consideradas procedimentos primitivos.
Em Lisp, criam-se combinação escrevendo uma sequência de expressões entre um par de parênteses. Uma expressão é um elemento primitivo ou uma outra combinação. A expressão (+ 1 2) é uma combinação dos elementos primitivos 1 e 2 através do procedimento +. Já no caso (+ 1 (* 2 3)) a combinação é ocorre entre 1 e (* 2 3), sendo esta última expressão uma outra combinação.
Não é difícil de ver que as únicas combinações com utilidade são aquelas em que as expressões correspondem a operadores e operandos. Por convenção, o Lisp considera que o primeiro elemento de uma combinação é um operador e os restantes são os operandos.
A notação que o Lisp utiliza para construir expressões (operador primeiro e operandos a seguir) é designada por notação prefixa. Esta notação costuma causar alguma perplexidade a quem inicia o estudo da linguagem, que espera uma notação mais próxima da que aprendeu em aritmética e que é usada habitualmente nas outras linguagens de programação. Nestas, a expressão (+ 1 (* 2 3)) é usualmente escrita na forma 1 + 2 * 3 (designada notação infixa) que, normalmente, é mais simples de ler por um ser humano. No entanto, a notação prefixa usada pelo Lisp tem largas vantagens sobre a notação infixa:
! É muito fácil usar operadores que têm um número variável de argumentos, como por exemplo: ! > (+ 1 2 3) ! 6 ! > (+ 1 2 3 4 5 6 7 8 9 10)
Quando a regra de indentação não é suficiente, usam-se pequenas variações, como seja colocar o operador numa linha e os operandos por debaixo, por exemplo:
(umaoperacaocomumnomemuitogrande 1 2 3 4)
A indentação é fundamental em Lisp pois é muito fácil escrever código complexo. A grande maioria dos editores preparados para Lisp (Emacs, por exemplo) formatam automaticamente os programas à medida que os escrevemos e mostram o emparelhamento de parênteses. Desta forma, após algum tempo de prática, torna-se muito fácil escrever e ler os programas, por mais complexa que possa parecer a sua estrutura.
Exercício 3
Converta as seguintes expressões da notação infixa da aritmética para a notação prefixa do Lisp: 1 + 2 - 3 1 - 2 * 3 1 * 2 - 3 1 * 2 * 3 (1 - 2) * 3 (1 - 2) + 3 1 - (2 + 3) 2 * 2 + 3 * 3 * 3
Exercício 4
Converta as seguintes expressões da notação prefixa do Lisp para a notação infixa da aritmética: (* (/ 1 2) 3) (* 1 (- 2 3)) (/ (+ 1 2) 3) (/ (/ 1 2) 3) (/ 1 (/ 2 3)) (- (- 1 2) 3) (- 1 2 3)
1.3 - Avaliação de Combinações
Como já vimos, o Lisp considera que o primeiro elemento de uma combinação é um operador e os restantes são os operandos.
O avaliador determina o valor de uma combinação como o resultado de aplicar o procedimento especificado pelo operador ao valor dos operandos. O valor de cada operando é designado de argumento do procedimento. Assim, o valor da combinação (+ 1 (* 2 3)) é o resultado de somar o valor de 1 com o valor de (* 2 3).
Como já se viu, 1 vale 1 e (* 2 3) é uma combinação cujo valor é o resultado de multiplicar o valor de 2 pelo valor de 3 , o que dá 6, e que somado a 1 dá 7.
(+ 1 2) 3 (+ 1 (* 2 3)) 7
Exercício 5
Calcule o valor das seguintes expressões Lisp: (* (/ 1 2) 3) (* 1 (- 2 3)) (/ (+ 1 2) 3) (/ (/ 1 2) 3) (/ 1 (/ 2 3)) (- (- 1 2) 3) (- 1 2 3) (- 1)
1.4 - Definição de Funções
Tal como em matemática, pode-se definir numa linguagem de programação a operação de elevar um número ao quadrado. Assim, se pretendermos determinar o produto de um número por ele próprio, escrevemos a combinação (* x x) sendo x o número, i.e.
(* 5 5) 25 (* 6 6)
36
Os números 5 e 6 e a operação * são elementos primitivos. As expressões (* 5 5) e (* 6 6) são combinações. À combinação genérica (* x x) queremos associar um nome, por exemplo, quadrado. Isso equivale a acrescentar uma nova função à linguagem.
A título de exemplo, vamos definir a função quadrado:
(defun quadrado (x) (* x x)) quadrado
Para se definirem novas funções em Lisp, é necessário criar uma combinação de quatro elementos. O primeiro elemento desta combinação é a palavra defun, que informa o avaliador que estamos a definir uma função. O nome defun é uma abreviatura de ``define function''. O segundo elemento é o nome da função que queremos definir, o terceiro elemento é uma combinação com os parâmetros da
Note-se que este ambiente apenas existe enquanto estamos a trabalhar com a linguagem. Quando terminamos, perde-se todo o ambiente. Isto implica que, se não queremos perder o trabalho que estivemos a escrever, devemos escrever as funções num ficheiro e ir passando-as para o Lisp. A grande maioria das implementações do Lisp permite fazer isto de forma automática.
1.5 – Funções básicas
( + ) ( Soma )
Note que 9/2 e 17/2 são números racionais que podem ser manipulados da mesma forma de inteiros e floats, e que a função soma não se restringe a apenas dois parâmetros.
( - ) ( Subtração )
( * ) ( Multiplicação )
( / ) ( Divisão )
( / 2 ) " 1/2 - inverso do número
Note que se houver mais de dois argumentos na divisão, o primeiro deles será dividido sucessivamente pelos demais.
sqrt ( Raiz quadrada )
( sqrt 4 ) " 2. ( sqrt 4.0 ) " 2. ( sqrt –4 ) " #C(0,0 2.0)
Se o argumento for um número negativo, a função não retornará um erro mas sim um número complexo na forma #C(3 2) que é equivalente a 3 + 2i.
expt ( Exponencial )
( expt 2 10 ) " 1024 ( expt 2 10.0 ) " 1024. ( expt 2/3 3 ) " 8/ ( expt –4 ½ ) " #C(1.22514845490862E-16 2.0)
Se algum dos argumentos for do tipo float, o resultado também será convertido para float, podendo ocorrer aproximações do número. No caso de 0.0, o resultado é aproximado para #C(1.22514845490862E-16 2.0).
log ( Logaritmo )
( log 1 ) " 0. ( log 10 ) " 2. ( log 10 2 ) " 3.
abs ( Valor Absoluto )
( abs 8 ) " 8 ( abs –8 ) " 8 ( abs –8.9 ) " 8.
truncate ( Trunca )
( truncate 13.5 ) " 13 0. ( truncate 0.7 ) " 0 0. ( truncate –1.7 ) " -1 -0.
Truncar é retornar o valor inteiro de um número. Note que a função retorna dois valores: o primeiro é o valor truncado, a parte inteira, e a segunda é a parte decimal do número, “o resto”.
Vamos inicializar quatro variáveis com quatro valores distintos para que os exemplos a seguir sejam executados corretamente.
( setf zero 0 one 1 two 2 three 3 four 4 ) " 4
( > ) ( Ordem Descendente )
( > 100 10 one 0.1 ) " T ( > 10 100 1 0.1 ) " NIL
Retorna verdadeiro se os elementos estão dispostos em ordem descendente.
( < ) ( Ordem Ascendente )
( < 1 two 4 ) " T
Retorna verdadeiro se os elementos estão dispostos em ordem ascendente.
( = ) ( Igual )
max ( Máximo )
( max 1 -2 3 4 5 6 7 ) " 7 ( max ( * 3 7) ( * 2 3 ) ) " 21
Retorna o argumento de maior valor.
min ( Mínimo )
( min 1 –2 3 4 5 6 7 ) " - ( min ( expt 2 10 ) ( expt 10 2 ) ) " 100
Retorna o argumento de menor valor.
evenp ( Par )
( evenp 17 ) " NIL ( evenp ( * 3 2 ) ) " T
Testa se o argumento é par.
oddp ( Ímpar )
( oddp 17 ) " T ( oddp two ) " NIL
Testa se o argumento é ímpar.
minusp ( Negativo )
( minusp 17 ) " NIL ( minusp –13 ) " T
Verifica se o argumento é um número negativo.
zerop ( Zero )
( zerop ( * 2 0 ) ) " T ( zerop ( + 2 0 ) ) " NIL ( zerop zero ) " T
Testa se o argumento é igual a zero. zerop é mais eficiente que seu equivalente ( = number 0).
1.7 – Avaliando funções
Em Lisp, + é uma função, assim uma expressão como (+ 2 3) é uma chamada de função envolvendo dois argumentos.
Quando Lisp avalia uma chamada de função, ele o faz em duas etapas: ! primeiramente os argumentos são avaliados, da esquerda para a direita. Neste contexto, cada argumento se "auto–avalia". Logo, 2 avalia para 2 e 3 valia para 3, e, assim, os valores dos argumentos são 2 e 3, respectivamente; ! os valores dos argumentos são passados a função designada pelo operador. Neste caso, é a função +, que retorna 5.
Exercício 6
Resposta
1.9 – Inserindo comentários
Para facilitar a compreensão de um código faz-se uso de comentários. Em Lisp, os comentários devem ser precedidos de ponto-e-vírgula “ ; ”, caracter que indica que a respectiva linha de código não deve ser avaliada pelo interpretador.
( defun novogosto ( nome ) ; adiciona nome no início da lista gosta ( setf gosta ( cons nome gosta ) ) ; deleta nome da lista detesta ( setf detesta ( remove nome detesta) ) )
Existem muitas operações cujo resultado depende da realização de um determinado teste. Por exemplo, a função matemática abs --que calcula o valor absoluto de um número-- equivale ao próprio número, se este é positivo, ou equivale ao seu simétrico se for negativo. Estas expressões, cujo valor depende de um ou mais testes a realizar previamente, permitindo escolher vias diferentes para a obtenção do resultado, são designadas expressões condicionais.
No caso mais simples de uma expressão condicional, existem apenas duas alternativas a seguir. Isto implica que o teste que é necessário realizar para determinar a via de cálculo a seguir deve produzir um de dois valores, em que cada valor designa uma das vias.
Seguindo o mesmo raciocínio, uma expressão condicional com mais de duas alternativas deverá implicar um teste com igual número de possíveis resultados. Uma expressão da forma caso o valor do teste seja 1, 2 ou 3, o valor da expressão é 10, 20 ou 30, respectivamente'' é um exemplo desta categoria de expressões que, embora seja considerada pouco intuitiva, existe nalgumas linguagens (Basic, por exemplo). Contudo, estas expressões podem ser facilmente reduzidas à primeira. Basta decompor a expressão condicional múltipla numa composição de expressões condicionais simples, em que o teste original é também decomposto numa série de testes simples. Assim, poderíamos transformar o exemplo anterior em:se o valor do teste é 1, o resultado é 10, caso contrário, se o valor é 2, o resultado é 20, caso contrário é 30''.
2.1 - Predicados
Desta forma, reduzimos todos os testes a expressões cujo valor pode ser apenas um de dois, e a expressão condicional assume a forma de ``se ...então ...caso contrário ...''. Nesta situação, a função usada como teste é denominada predicado e o valor do teste é interpretado como sendo verdadeiro ou falso. Nesta óptica, o fato de se considerar o valor como verdadeiro ou falso não implica que o valor seja de um tipo de dados especial, como o booleano ou lógico de algumas linguagens (como o Pascal), mas apenas que a expressão condicional considera alguns dos valores como representando o verdadeiro e os restantes como o falso.
Em Lisp, as expressões condicionais consideram como falso um único valor. Esse valor é representado por nil. Qualquer outro valor diferente de nil é considerado como verdadeiro. Assim, do ponto de vista de uma expressão condicional, qualquer número é um valor verdadeiro. Contudo, não faz muito sentido para o utilizador humano considerar um número como verdadeiro ou falso, pelo que se introduziu uma constante na linguagem para representar verdade. Essa constante representa-se por t.
(and (or (> 2 3) (not (= 2 3))) (< 2 3)) (not (or (= 1 2) (= 2 3))) (or (< 1 2) (= 1 2) (> 1 2)) (and 1 2 3) (or 1 2 3) (and nil 2 3) (or nil nil 3)
2.3 - Seleção simples
O if é a expressão condicional mais simples do Lisp. O if determina a via a seguir em função do valor de uma expressão lógica. A sintaxe do if é:
(if condição consequente alternativa )
Para o if, a condição é o primeiro argumento, o consequente no caso de a condição ser verdade é o segundo argumento e a alternativa no caso de ser falso é o terceiro argumento.
(if (> 4 3) 5
5
Uma expressão if é avaliada determinando o valor da condição. Se ela for verdade, é avaliado o consequente. Se ela for falsa é avaliada a alternativa.
Exercício 8
Defina uma função soma-grandes que recebe três números como argumento e determina a soma dos dois maiores.
Exercício 9
Escreva uma função que calcule o factorial de um número.
A função fact é um exemplo de uma função recursiva, i.e., que se refere a ela própria.
2.4 - Seleção Múltipla
Além do if, existe outra expressão condicional em Lisp. O cond é uma versão mais potente que o if. É uma espécie de switch-case do C. A sua sintaxe é:
(cond ( condição-1 expressão-1 )
( condição-2 expressão-2 )
( condição-n expressão-n ))
Designa-se o par ( condição-i expressão-i ) por cláusula. O cond testa cada uma das condições em sequência, e quando uma delas avalia para verdade, é devolvido o valor da expressão correspondente, terminando a avaliação. Um exemplo será:
(cond ((> 4 3) 5) (t 6)) 5
O cond permite uma análise de casos mais simples do que o if.
(defun teste (x y z w) (cond ((> x y) z) ((< (+ x w) (* y z)) x) ((= w z) (+ x y)) (t 777)))
A função equivalente usando if seria mais complicada.
(defun teste (x y z w) (if (> x y) z (if (< (+ x w) (* y z)) x (if (= w z) (+ x y) 777))))