Apêndice 2 - Linguagem de programação Mathematica (breve referência) Neste apêndice abordamos o sistema computacional Mathematica, mencionando, basicamente, somente aqueles aspectos que são essenciais para entender os programas, escritos na linguagem de programação Mathematica, que são apresentados ao longo deste texto. Embora o sistema Mathematica suporte vários paradigmas da programação, iremos aqui limitar-nos a apresentar apenas as construções essenciais para a programação recursiva e imperativa, não fazendo qualquer referência explícita nem à programação funcional, nem à reescrita (paradigma particularmente apropriado para a chamada computação simbólica). Para uma introdução (mais completa) ao sistema Mathematica, e à programação neste nos vários paradigmas referidos, pode consultar-se1 p.ex. [8]. Expressões. As expressões matemáticas, numéricas e simbólicas, podem ser escritas no sistema Mathematica seguindo uma sintaxe muito próxima da sintaxe usual em matemática. No que respeita às operações aritméticas, os símbolos dos operadores são os usuais: + (adição), (subtracção), * (multiplicação) e / (divisão)2. Os operadores de comparação são também denotados pelos símbolos usuais (<, >, <= ou ≤, >= ou ≥), com excepção de que se usa == em vez de3 =, para o teste de igualdade (numérica4), e se usa != em vez de ≠, para a desigualdade5. Já no que respeita aos conectivos Booleanos (conectivos lógicos), embora se possa ir a uma tabela de símbolos e usar os símbolos usuais da lógica e da matemática, como ¬, ∧ e ∨, os símbolos padrão no Mathematica não são esses, usando-se !p (ou Not[p]) para a “negação de p” p&&q (ou And[p,q]) para a “conjunção de p e q” p||q (ou Or[p,q]) para a “disjunção de p e q” 1 Especificamente sobre o sistema Mathematica pode consultar-se: “S. Wolfram, The Mathematica book, Wolfram Media, 3ª edição, 1996”. 2 Podemos ainda recorrer a uma tabela de símbolos para escrever quocientes, potências, raízes quadradas (bem como integrais, somatórios, etc.) tal como é ususal em matemática, e se ilustra a seguir: 2 , x 2 , 3 x2 , 3 4, 20 20 i=2 1 ∑ i 2, ∫ 1 dx, etc. x 3 Que tem outro significado na linguagem de programação Mathematica, sendo aí usado como sinal de atribuição (como ilustraremos à frente). € 4 Existe ainda um operador para testar a igualdade/identidade simbólica (operador denotado por ===, sendo a sua negação denotada por =!=), mas não abordaremos aqui as diferenças entre os dois testes de igualdade (limitando-nos neste texto a considerar a igualdade numérica). 5 Em relação à desigualdade, podemos ir à tabela de símbolos e escrever também ≠. 493 Refira-se a propósito que a avaliação no Mathematica de uma conjunção p&&q é sequencial: se a avaliação da expressão booleana p retornar False, já não é avaliada a expressão booleana q (sendo retornado False). E a avaliação de uma disjunção também se processa analogamente, de uma forma sequencial. Funções predefinidas. O Mathematica dispõe de imensas funções predefinidas. O seu nome começa sempre por uma Maíuscula e é, para as funções conhecidas, o que se espera, escrito em língua inglesa. Por exemplo, as funções cosseno e seno são descritas, respectivamente, pelos nomes Cos e Sin. Se invocarmos a função cosseno aplicada ao ponto π/4: Cos[ π ] 4 obtemos € 1 2 uma vez que o Mathematica, sempre que possível, trabalha com valores exactos. € Se quisermos obter um valor aproximado devemos recorrer à função N, que por defeito dá um valor aproximado do resultado com 6 algarismos significativos. Se quisermos obter uma maior precisão, devemos escrever, como segundo argumento da função N, o número de dígitos de precisão que pretendemos. Assim, invocando N[Cos[ π ]] e 4 N[Cos[ π ],10] 4 obtém-se, respectivamente: € 0.707107 e 0.7071067812 € Note-se que podemos obter informação sobre o que faz uma função predefinida, avaliando ? nome da função Se avaliarmos ?N obtém-se N[expr] gives the numerical value of expr. N[expr, n] attempts to give a result with n-digit precision Parênteses rectos e curvos e chavetas. Saliente-se que, como os exemplos anteriores ilustraram, a aplicação de uma função faz-se (no sistema Mathematica) colocando os argumentos (separados por vírgulas, no caso de ser mais do que um) entre parênteses rectos, e não entre parênteses curvos (como é usual em matemática)! Os parênteses curvos são utilizados apenas para agrupar expressões (podendo ser, desse modo, usados para ultrapassar as usuais precedências dos operadores), enquanto que as chavetas são usadas para a construção de listas (como veremos). 494 A função predefinida Timing e a constante Null. Uma função que utilizamos no texto (no capítulo 12) é a função Timing. Uma invocação Timing[exp] retorna (uma lista da forma) {tempo,valor} onde o tempo refere o tempo gasto em segundos na6 avaliação da expressão7 exp, e o valor refere o valor obtido na avaliação da expressão exp. Se escrevermos Timing[exp;] é omitido do output o valor da expressão exp, sendo retornado (uma lista da forma) {tempo,Null} onde Null é uma constante que denota uma expressão (atómica) especial sem qual valor. Se mandarmos avaliar Null nada é escrito no ecrã. Tipos. Em Mathematica existem os seguintes tipos básicos: • Tipos numéricos: • Integer (os inteiros, expressos na forma usual; p.ex. 3, -4, etc.) • Rational (racionais, expressos como um quociente de inteiros; p.ex. 1/3, etc.) • Real (reais, expressos recorrendo ao ponto decimal e não à vírgula; p.ex. 2., 3.4, etc.) • Complex (complexos, expressos na forma usual, com I designando a −1 ; p.ex. 2+3I, etc.) • Tipo Symbol (o tipo dos nomes): Os elementos deste tipo são nomes formados por uma letra eventualmente seguida de uma ou mais € letras ou números. Alguns destes nomes representam constantes e funções predefinidas no Mathematica. Os restantes nomes podem ser usados para variáveis. Como os nomes predefinidos começam sempre por uma maíuscula, é conveniente que os nomes escolhidos pelo utilizador para variáveis (ou para nomes de funções) comecem por uma minúscula, evitando assim conflitos com os identificadores predefinidos (de qualquer forma, quando tais conflitos ocorrem, o Mathematica avisa-nos). Refira-se ainda, a propósito, que o Mathematica é uma linguagem de programação “atipada”: as variáveis utilizadas não são declaradas como sendo de um certo tipo, podendo denotar valores de diferentes tipos. O Mathematica possui regras que permitem avaliar as várias operações, sendo uma expressão avaliada por aplicação dessas regras; quando não existe qualquer regra disponível que seja aplicável à avaliação (reescrita) de uma dada expressão, ou quando a sua avaliação envolve a aplicação 6 Não incluindo, nesse tempo, o tempo gasto a formatar e escrever o output. 7 Nos casos considerados no capítulo 12, a expressão em causa (a que é aplicado o Timing) corresponde à invocação de uma função/programa. 495 de uma operação a valores “inadequados” (como uma divisão por zero), pára a avaliação da expressão em causa, e esta é retornada na forma que estava no momento em avaliação (no último caso, normalmente acompanhada de uma mensagem de erro). • Tipo String (o tipo das mensagens): Os elementos deste tipo são (quaisquer) sequências de caracteres entre aspas, e podem ser encarados como mensagens. Eventuais operações que se encontrem numa mensagem não são avaliadas. Avaliando p.ex. 2+3 e “2+3” obtém-se, respectivamente: 5 e 2+3 Toda a informação que não seja de um destes tipos tem de ser representada no Mathematica recorrendo às listas, que analisaremos à frente. Atribuição (imediata). Há duas formas de atribuição que podem ser usadas no Mathematica: a atribuição imediata e a diferida. Aqui falaremos apenas da primeira. Uma regra de atribuição (imediata) toma a forma nome = exp e a execução de uma atribuição deste tipo consiste em avaliar a expressão exp (do lado direito do sinal =) e em guardar o valor8 assim obtido na variável denotada pelo nome que se encontra no lado esquerdo do sinal =. Estas regras, que se se podem ler como se segue “atribuir (o valor de) exp à variável nome“, são normalmente usadas para “memorizar” valores em variáveis. Por exemplo, uma atribuição da forma a = 3 guarda em (na variável) a o valor 3. A partir desse momento, em qualquer expressão em que a ocorra, a denota o valor 3. O valor guardado na variável a pode ser alterado em virtude de uma nova atribuição a essa variável. Se efectuarmos a atribuição a = a+1 então a passará a denotar o valor 4. Podemos ainda apagar o valor memorizado em a efectuando9 Clear[a] A expressão exp do lado direito de uma atribuição nome = exp 8 Valor que é escrito no ecrã, salvo se a atribuição nome=exp for seguida de um ;, em cujo caso nada é escrito no ecrã (ver o que se diz a seguir sobre o sequenciamento). 9 Após a avaliação da expressão Clear[a], a variável a passa a denotar-se a si própria (avaliando a, obtém-se a). Por outro lado, podemos mesmo remover a variável a da nossa sessão de trabalho, efectuando Remove[a]. 496 pode ser a definição de uma função (ver à frente), em cujo caso a atribuição em causa é vista como estando a dar aquele nome à função. Sequenciamento. O sequenciamento de acções é efectuado recorrendo ao ponto e vírgula (como é usual nas linguagens de programação). Se avaliarmos, por exemplo a = 5; b = a+2; a+b obtém-se (é escrito no ecrã, como output) 12 Uma sequência da forma (com um ponto e vírgula no fim) a = 7; b = a+1; a+b; é interpretada como a = 7; b = a+1; a+b; Null e corresponde a efectuar, em sequência: atribuir 7 a a; atribuir 8 (o resultado da avaliação de a+1) a b; avaliar a expressão a+b; e, finalmente, avaliar a expressão Null. Como o Mathematica apenas retorna como output, escrito no ecrã, o valor da última expressão avaliada, e como Null denota uma expressão especial sem qualquer valor, se avaliarmos a = 7; b = a+1; a+b; nada é escrito no ecrã. No entanto as acções referidas foram efectuadas. Se mandarmos avaliar a variável b obtemos (o valor que nela está guardado de momento) 8 Podemos também questionar o Mathematica sobre o que está associado a um determinado nome introduzido pelo utilizador, escrevendo ?b obtendo-se neste caso Global`b b = 8 o que mostra claramente que a atribuição acima (b=a+1) foi efectuada. A mensagem Global`b significa informalmente que b é um dos nomes introduzidos pelo utilizador10 na sessão de trabalho em curso. Escrita de resultados. Como já referimos, o Mathematica apenas escreve no ecrã o valor da última expressão avaliada. Assim, se queremos escrever no ecrã um resultado intermédio de uma computação (ou escrever uma mensagem no meio de uma computação), devemos recorrer ao comando (específico) de impressão Print. A execução de Print[exp1 ,...,expn ] 10 E não removido, por recurso a um Remove (ver nota de rodapé anterior). 497 tem como efeito a escrita sucessiva, na mesma linha, do valor das expressões exp1 , ..., expn (seguida de mudança de linha). Listas. Uma lista não é mais do que uma sequência de elementos (admitindo repetições). A importância das listas no Mathematica decorre do facto, já referido, de toda a informação que não seja de um tipo básico ter de ser representada no Mathematica recorrendo a listas. Por essa razão, o Mathematica disponibiliza um tipo de listas extremamento rico, com imensas funções predefinidas11 que permitem manipular as listas de uma forma simples e extremamente eficiente. Uma lista pode ser definida directamente em extensão, enumerando explicitamente os elementos que a compõem, pondo estes, separados por vírgulas, entre chavetas, ou como argumentos da função predefinida (de construção de listas) List. Por exemplo, a lista vazia é representada por {} ou por List[] e a expressão {5, D[Sin[x],x], {3,4}, a} ou List[5, D[Sin[x],x], {3,4}, a] representa a lista cujo primeiro elemento é o número 5, o segundo elemento é a expressão da derivada do sen(x) (i.e. a expressão Cos[x]), o terceiro elemento é a lista formada pelo 3 e pelo 4 , e o último elemento é a letra a (que poderá estar a denotar-se a si própria, ou poderá estar a denotar um outro qualquer valor, que nela tenha sido guardado em virtude de uma atribuição anterior). Como o exemplo anterior ilustra, numa lista podemos guardar elementos de diferentes tipos. Listas formadas só por listas, todas do mesmo comprimento, podem ser encaradas como matrizes, e a elas serem aplicadas operações específicas de matrizes (como soma, produto, determinante, etc.). Uma lista pode também ser definida por compreensão, através da referência à regra de cálculo que permite gerar os seus elementos. Para esse efeito o Mathematica disponibiliza três funções predefinidas: Table, Array e Range. Questionando o Mathematica sobre estas funções, obtém-se (onde se suprime parte da informação disponibilizada, por não ser para aqui essencial): ? Range Range[imax] generates the list {1, 2, ... , imax}. Range[imin, imax] generates the list {imin, ... , imax}. Range[imin, imax, di] uses step di ? Array Array[f, n] generates a list of length n, with elements f[i]. Array[f, {n1, n2, ... }] generates an n1 by n2 by ... array of nested lists, with elements f[i1, i2, ... ]. 11 No que se segue referiremlos apenas algumas dessas funções. 498 ? Table Table[expr, {imax}] generates a list of imax copies of expr. Table[expr, {i, imax}] generates a list of the values of expr when i runs from 1 to imax. Table[expr, {i, imin, imax}] starts with i = imin. Table[expr, {i, imin, imax, di}] uses steps di. Table[expr, {i, imin, imax}, {j, jmin, jmax}, ... ] gives a nested list. The list associated with i is outermost Uma lista pode também ir sendo construída, através da manipulação de outras listas, recorrendo nomeadamente a operações de inserção e de extracção de elementos. Seguem-se algumas das principais operações disponíveis para esse fim: • Append[w,x] (onde w denota uma lista) Constrói uma lista que é igual à lista que se obtém quando se acrescenta (o valor denotado por) x no fim da lista (denotada por) w. • Prepend[w,x] (onde w denota uma lista) Retorna a lista que se obtém quando se acrescenta x no início da lista em (denotada por) w. • Insert[w,x,n] (onde w denota uma lista e n um inteiro) Retorna a lista que se obtém quando se insere x na lista12 em w na posição n (se n for igual a zero, obtém-se um erro; se n for igual ao número de elementos de w mais um, x é colocado no fim da lista em w; se n for maior que o número de elementos de w mais um, obtém-se um erro; se n for menor que zero, as posições são contadas da direita para a esquerda13). Por exemplo, após a avaliação de a=Append[{14},7];b=Insert[a,4,3];c=Insert[a,4,2];d=Insert[a,4,-1] a guardará a lista {14,7}, b guardará a lista {14,7,4}, c a lista {14,4,7} e d a lista {14,7,4}. • Join[w1,w2,...,wn] (onde w1,w2,...,wn denotam listas) Retorna a lista que se obtém quando se junta as listas em w1,w2,...,wn. • Rest[w] (onde w denota uma lista) Retorna a lista que se obtém da lista (denotada por) w , quando se retira desta o primeiro elemento (obtendo-se um erro se w estiver vazia). • Delete[w,n] (onde w denota uma lista e n um inteiro) Retorna a lista que se obtém quando se retira da lista em w o elemento que está na posição n. 12 Note-se que a lista argumento destas operações não é alterada pela sua execução (salvo se o resultado lhe estiver a ser atribuído através de um comando/instrução/regra de atribuição). 13 Este procedimento é análogo para as outras operações sobre listas que têm um argumento que denota uma posição da lista. Por essa razão não o referiremos mais. Igualmente não referiremos mais, em geral, as possíveis situações de erro, para não alongar a exposição. 499 • Take[w,n] (onde w denota uma lista e n um inteiro) Retorna a lista que se obtém quando se extrai da lista em w os primeiros n elementos14 (i.e. Take[w,n] denota a lista formada pelos primeiros n elementos da lista em w). • Drop[w,n] (onde w denota uma lista e n um inteiro) Retorna a lista que se obtém quando se retira da lista em w os primeiros n elementos. (Para retirar o último elemento de lista em w, podemos escrever Drop[w,-1].) Dispomos igualmente de funções que nos permitem aceder aos elementos que estão na primeira e na última posição de uma lista, bem como ao elemento que está numa posição especificada da lista: • First[w] (onde w denota uma lista) Retorna o primeiro elemento da lista em w (obtendo-se erro se w estiver vazia). • Last[w] (onde w denota uma lista) Retorna o último elemento da lista em w. • w[[n]] (onde w denota uma lista e n um inteiro) Retorna o elemento que está na posição n da lista em w. Se w for uma lista de listas, então w[[n]][[k]] pode ser abreviado por w[[n,k]]. Por exemplo, se w for a lista {{6,5},{7,8,4},{6}}, então w[[2,3]] denotará o terceiro elemento da lista que está na segunda posição de w, i.e. o valor 4. Podemos também alterar o elemento que está numa dada posição de uma lista, como se segue • ReplacePart[w,x,n] (onde w denota uma lista e n um inteiro) Retorna a lista que se obtém quando se substitui na lista em w o elemento que está na posição n por x. E podemos alterar a lista que está guardada numa determinada variável, através da atribuição de uma nova lista à variável, como em a={7,-25,18} ou alterando apenas o elemento que está numa dada posição n da lista guardada na variável a, através de uma atribuição da forma a[[n]]=x (que tem o mesmo efeito que a=ReplacePart[a,x,n]). Por exemplo, supondo que a guarda a lista {7,-25,18}, então, após a atribuição a[[2]]=0 a variável a passará a denotar a lista {7,0,18}. Finalmente, uma referência apenas a outras duas funções disponíveis sobre listas, muito úteis e que utilizamos em alguns programas Mathematica apresentados no texto: • Length[w] (onde w denota uma lista) Retorna o comprimento (o número de elementos) da lista em w. Por exemplo, Length[{5,{6,7,-4,8},5,{}}] retornará 4. 14 Os últimos n elementos, se n for negativo. 500 • Select[w,f] (onde w denota uma lista e f um predicado15) Retorna a lista formada pelos elementos da lista w que satisfazem o predicado16 f. Por exemplo, Select[{5,6,7,4,8},EvenQ] retornará a lista formada pelos elementos da lista argumento que satisfazem o predicado EvenQ (no caso um predicado predefinido17, que retorna True se aplicado a um número par e False no caso contrário), ou seja retornará a lista {6,4,8}). Funções. Pode considerar-se que programar em Mathematica consiste em definir funções. Uma função pode ser definida por abstracção funcional, ou através de atribuições paramétricas diferidas. Neste apêndice apenas abordaremos a técnica da definição de funções por abstracção funcional. Para definir funções, por (o que denominaremos de) abstracção funcional, recorre-se em Mathematica à construção Function[{p1,...,pn},corpo] onde {p1,...,pn} é uma lista (eventualmente vazia) de nomes, que representam os parâmetros (formais) da função, e o corpo é uma expressão ou sequência de expressões Mathematica (simples ou complexas), que representa a regra de cálculo do resultado da função. Quando a função tem apenas um parâmetro p, podemos escrever simplesmente Function[p,corpo] Por exemplo, a função muito simples a seguir, associa a cada número o seu quadrado Function[n, n^2 (* ou, indo à tabela de símbolos, n2 *)] (Note-se que podemos escrever comentários, no meio de código Mathematica, colocando o comentário entre (* e *) 18.) Saliente-se que a entidade anterior é uma função, pelo que podemos aplicá-la a um argumento, mesmo sem lhe dar qualquer nome. Por exemplo, avaliando Function[n,n^2][5] obtém-se 25 15 Uma função predefinida, ou definida pelo utilizador, com resultado True ou False. 16 Um elemento x satisfaz um predicado f se f[x] retornar True. 17 A generalidade dos predicados predefinidos terminam em Q (que podemos ver como mnemónico de “Query”). Existem, contudo, também predicados predefinidos que não terminam em Q, mas não abordaremos aqui as diferenças entre os dois tipos de predicados predefinidos. 18 Um Notebook Mathematica está organizado em células, delimitadas por um parêntese recto no lado direito da janela, tendo cada célula um tipo associado, que pode ser texto (text), entrada (input), saída (outpu), gráficos (graphics), entre outros. Embora, por omissão, quando escrevemos no Mathematica a célula seja de inpurt, podemos definir tal célula como sendo p.ex. de text, o tipo natural para escrita de texto ou comentários longos. O input (i.e. o código/texto escrito numa célula de input) é avaliado premindo simultaneamente as teclas shift e return, sendo o resultado retornado numa célula de output. Como se refere em cima, comentários (normalmemente curtos) podem ainda ser escritos no meio de código de input, colocando-os entre (* e *): qualquer texto que esteja, numa célula de input, entre (* e *) não é avaliado pelo Mathematica. 501 Mas, naturalmente, podemos dar nomes às funções que definimos, o que facilita a sua invocação (evitando, nomeadamente, ter de estar a repetir a definição da função em suas sucessivas invocações): tal é feito através de uma atribuição da função ao nome pretendido. Por exemplo, chamando à função atrás de quad (abreviatura de quadrado), através da atribuição19 quad = Function[n, n^2]; pode em seguida invocar-se facilmente a função e escrever p.ex. quad[3]+quad[quad[2]] expressão cuja avaliação retornará 25. No caso da função definida ter mais do que um parâmtetro20, como no exemplo a seguir med = Function[{x,y},(x+y)/2]; então numa sua invocação temos de indicar um argumento por cada um dos parâmetros, separando-os por vírgulas, como se ilustra a seguir med[5,1] É importante perceber o modo como se processa a avaliação da invocação de uma função. Aquando da invocação de uma função os parâmteros são substituídos no corpo da função (em todo o sítio onde aí ocorram), pelos correspondentes argumentos, a expressão assim obtida é avaliada e o valor assim obtido é retornado como resultado dessa invocação. Por exemplo, a invocação med[5,1] anterior, conduziria à expressão (5+1)/2 expressão que seria avaliada, sendo o seu valor retornado como output (e escrito no ecrã) 3 Assim, ao contrário do que se passa na maioria das linguagens de programação de carácter imperativo, os parâmetros não podem ser usados como variáveis locais. Por exemplo, se introduzirrmos a definição f = Function[n,n=n+1;n^2]; e em seguida avaliarmos f[4] obtemos uma mensagem de erro, e não o valor 25, uma vez que o que o Mathematica faz é substituir o parâmetro n pelo argumento 4, no corpo da função f, conduzindo à sequência 4=4+1;4^2 que é em seguida avaliada, dando origem a uma situação de erro, em virtude de não podermos atribuir 4+1 a 4 (só podemos efectuar atribuições a variáveis). 19 O ponto e vírgula ; no fim da atribuição serve para evitar que, após a avaliação dessa atribuição, apareça no ecrã a função definida Function[n,n2] e que foi guardada no nome quad (recorde-se o que se disse atrás acerca de uma atribuição imediata e do sequenciamento, com um ponto e vírgula no fim). 20 Em cujo caso, recorde-se, os parâmetros têm de ser apresentados na forma de uma lista, colocando-os entre chavetas, separados por vírgulas. 502 Expressões condicionais. Muitas vezes pretendemos definir uma função cujo resultado depende de uma certa condição (a que por vezes se chama em matemática de funções definidas por ramos). Tal é nomeadamente a situação em qualquer função/programa recursivo, em que o resultado é definido directamente para o chamado caso base da recursão, sendo para os outros casos definido recursivamente (o chamado passo da recursão) 21. Para esse efeito recorremos às chamadas expressões condicionais, que no Mathematica tomam a forma If[condição, expressão1, expressão2] e que se podem ler como se segue: “se (if) condição, então (then) expressão1, senão (else) expressão2”. Como a leitura anterior sugere, a avaliação de uma expressão condicional If[condição, expressão1, expressão2] corresponde a: i) avaliar a condição; ii) e, se esta for verdadeira22, retornar como resultado o valor da expressão1; iii) caso contrário, retornar como resultado o valor da expressão2. Recorrendo a esta construção é muito simples definir, por exemplo, um programa recursivo para o cálculo do factorial de um número natural (programa cuja eficiência é analisada no capítulo 12): factRec = Function[n, If[n==0, 1, n*factRec[n-1]] ]; Construções essenciais para a programação imperativa. Para terminar este apêndice, vejamos apenas (algumas d)as principais contruções que o Mathematica disponibiliza para a chamada programação imperativa, e que são utilizadas nos programas apresentados no texto (e, nomeadamente, no capítulo 12). Como se refere em [8], no paradigma da programação imperativa utiliza-se (normalmente) uma variável para guardar o resultado desejado, bem como outras variáveis auxilares do processo de cálculo. O valor memorizado nessas variáveis é sucessivamente alterado (actualizado), até se obter o estado final pretendido, através do encadeamento de acções elementares (tipicamente atribuições) que são organizadas por intermédio de três formas básicas de composição de acções: sequencial, alternativa e iterativa. A composição sequencial permite mandar executar acções (e avaliar expressões) em sequência, recorrendo ao ;, como já se abordou atrás. 21 O caso base da recursão corresponde à condição inicial, no caso das sucessões definidas por recorrência, e o passo da recursão à equação de recorrência: veja-se o capítulo 8, onde a problemática da definição recursiva de funções é abordada (e fundamentada). 22 I.e. se o resultado da avaliação da condição for True. 503 A composição de acções em aternativa, em função de uma dada condição, é efectuada recorrendo23 à construção If, acabada de referir, mas que no contexto da programação imperativa é vista não tanto como uma alternativa entre (a avaliação de) duas expressões, mas mais com o ênfase na execução em alternativa de duas acções (ou sequências de acções, simples ou complexas). Mais concretamente, e numa linguagem informal, podemos dizer que na programação imperativa se recorre tipicamente a duas formas de if’s: • o chamado if-then-else, da forma If[condição, acção1, acção2] cuja execução corresponde a: i) avaliar a condição; ii) e, se esta for verdadeira, executar a acção1 (que poderá ser uma sequência de acções); iii) caso contrário, executar a acção2 (que poderá também ser uma sequência de acções). • e o chamado if-then, da forma If[condição, acção1] (ou If[condição, acção1,]) que pode ser visto como uma abreviatura de If[condição, acção1,Null] e cuja execução corresponde (portanto) a: i) avaliar a condição; ii) e, se esta for verdadeira, executar a acção1 (que poderá ser uma sequência de acções); iii) caso contrário, “não fazer nada” 24 (avaliar Null, que não produz qualquer resultado). Finalmente, com a composição iterativa (ou repetitiva) pretende-se mandar executar repetidamente uma certa acção enquanto uma certa condição é verdadeira (ou um número especificado de vezes). É esta forma de composição de acções, em iteração, que dá poder à programação imperativa, desempenhando de algum modo neste paradigma o papel da recursão no paradima da programação recursiva. As composições iterativas podem ser obtidas recorrendo a três construções do Mathematica: While, Do e For. Como nos programas apresentados neste texto apenas se utiliza a construção While, só abordaremos esta construção iterativa neste apêndice. A construção While toma a forma While[condição,acção] e a sua execução corresponde a: i) avaliar a condição; ii) e, se esta for falsa, “não fazer nada” (terminando a execução do While25); iii) caso contrário, é executada a acção (que poderá ser uma acção simples, como uma atribuição, ou uma sequência de acções, um If, ou um outro While), e em seguida volta-se a executar o mesmo While. 23 Existem outras construções de carácter alternativo, mas que não abordaremos aqui. 24 Concluindo a execução do If em causa, e passando à execução da acção que se seguir a esse If no programa em questão (isto no caso de se lhe seguir alguma outra acção). 25 Passando à execução da instrução/acção que se lhe seguir no programa (se existir alguma). 504 Isto é, na execução de um While[condição,acção] a acção é executada sse a condição for verdadeira, em cujo caso a acção é executada repetidamente, até que a (avaliação da) condição dê falso26, altura em que a execução do While pára. Refira-se, antes de prosseguir, que por vezes usamos o termo ciclo para designar um While[condição,acção], em cujo caso a acção referida é chamada do passo do ciclo, designando-se ainda a condição em causa da guarda do ciclo (terminologia que usamos algumas vezes neste texto, nomeadamente no capítulo 12). Se mandarmos avaliar um If é retornado como resultado o valor da última expressão avaliada. Por exemplo, avaliando x=2; y=3; If[x<0, x*y, x-y] obtém-se o valor -1, resultante da avaliação da expressão x-y. E, se avaliarmos x=2; y=3; If[x>0, y=y+1; x=x+y, y=y-1; x=x-y] para além das alterações (dos valores das variáveis x e y) decorrentes das atribuições efectuadas, é retornado como resultado o valor da última expressão avaliada, i.e. o valor 6 (resultante da avaliação da expressão x+y, avaliação efectuada no âmbito da execução da atribuição x=x+y). Pelo contrário, se avaliarmos x=7; y=3; While[x>0, y=y+1; x=x-y] nada aparece no ecrã: a avaliação de um While não se traduz pela produção de um resultado explícito (retornando Null). Deste modo, os efeitos de um While são (sempre) obtidos (colateralmente), por alteração de (do valor guardado em) variáveis, através de atribuições efectuadas no seu passo. (Qual o valor das variáveis x e y após a avaliação da sequência de instruções anterior?) De posse destas construções, torna-se já possível construir qualquer programa imperativo. No entanto, uma última observação é importante para os codificar no Mathematica. Considere-se, por exemplo, a sequência de instruções/acções a seguir r=1; i=1; While[i<=n, r=r*i; i=i+1] Facilmente se verifica, que após a execução desta sequência de instruções, está guardado na variável r o factorial do valor que esteja guardado na variável n (supondo que este é um número natural). No entanto, como o While não retorna qualquer valor, se mandarmos avaliar o código anterior nenhum valor é retornado. Assim, o que se faz em geral, no Mathematica, para retornar o resultado pretendido, é avaliar no final a variável que guarda esse resultado27. 26 Se tal nunca acontecer, então a execução do While não termina. 27No caso em que o resultado desejado não está guardado (numa só) variável, sendo obtido por combinação de valores guardados em várias variáveis, então é avaliada no final a expressão que traduz tal combinação. 505 De posse desta informação, já é simples construir uma função que, se aplicada a um número natural, retorna o seu factorial: factImp = Function[n, r=1; i=1; While[i<=n, r=r*i; i=i+1]; r ]; e se mandarmos avaliar p.ex. factImp[4] obtém-se 24 A invocação anterior, embore calcule correctamente o factorial do argumento, tem contudo efeitos colaterais (que, pelo menos no caso anterior, não são desejados28). De facto, após a invocação anterior, se mandarmos avaliar a variável i, obtém-se 5. O que se passa é que as sucessivas atribuições a29 i, decorrentes da avaliação de factImp[4], repercutiram-se para além desta avaliação, uma vez que não especificámos que as variáveis utilizadas deviam ser consideradas como variáveis locais, a utilizar apenas no cálculo pretendido do factorial de n. A construção Module[{nome1,...,nomen}, expressão] especifica que os nomes30 mencionados devem ser tratados como locais à avaliação da31 expressão referida. E, definindo a função para o cálculo do factorial (de forma imperativa) como se segue 28 No paradigma da programação imperativa tira-se muitas vezes partido desses efeitos colaterais, em situações em que estes são devidamednte “contextualizados”, mas sai fora do âmbito deste apêndice abordar esses aspectos metodológicos ligados à programação imperativa (veja-se p.ex. [8]). 29 Ao definir uma função podemos recorrer não só a funções predefinidas, como a qualquer função previamente introduzida pelo utilizador na sessão do Mathematica em curso. Podemos ainda utilizar variáveis que não são definidas como variáveis locais (do modo que se refere a seguir), mas eventuais alterações do valor dessas variáveis, decorrentes de atribuições a elas efectuadas no decorrer de uma invocação da função, repercutem-se para além dessa invocação. 30 Saliente-se que mesmo que seja um só nome, ele terá de estar entre chavetas. Por outro lado, refira-se que os nomes especificados podem ser utilizados, no cálculo efectuado (dentro do Module), como variáveis (onde se memorizam valores durante esse cálculo), ou como nomes de funções (aí definidas), mas em qualquer caso como nomes locais, em que as suas “definições”, efectuadas no cálculo realizado dentro do Module, se “extinguem” no fim desse cálculo. 31 Onde o termo expressão está aqui a ser usado em sentido amplo, podendo tratar-se quer de uma expressão matemática simples, quer de uma sequência de instruções/acções do tipo das que temos estado a referir, e que são tipicamente usadas nos programas imperativos. 506 factImp = Function[n, Module{r,i}, r=1; i=1; While[i<=n, r=r*i; i=i+1]; r ]]; já não se obtêm os efeitos colaterais referidos. Se avaliarmos, por exemplo i=1; factImp[4] obtém-se como reultado (como output) 24 mas o valor da variável32 i permanece inalterável. Se avaliarmos i obtém-se 1 Com esta (muito breve) introdução ao sistema Mathematica, deverá ser possível entender os programas apresentados no capítulo 12, mesmo que não se tenha tido qualquer contacto prévio com tal sistema computacional, e mesmo que não se tenha prática de programação33 (cumprindo-se assim o objectivo deste segundo, e último, apêndice). 32 Variável que é vista como sendo diferente da variável local i, utilizada para o cálculo de factImp[4]. 33 Estamos aqui a referir-nos a entender (ler) os programas apresentados. Não se afirma que, só com esta introdução à linguagem de programação Mathematica, será fácil a qualquer pessoa, sem nenhuma prática de programação, construir (por si) todos os programas apresentados no capítulo 12. Não nos debruçámos aqui sobre qualquer metodologia para a construção de programas: não é objectivo deste texto, ensinar a programar. 507