Em aulas anteriores vimos que as funções permitem-nos abstrair algoritmos e procedimentos de cálculo. Notar que uma função permite-nos computar o algoritmo subjacente sobre quaisquer parâmetros concretos (compatíveis) e, portanto, corresponde a uma abstracção procedimental que nos permite introduzir novos conceitos. Veremos de seguida como introduzir ainda conceitos mais abstractos.
Funções como parâmetros
Consideremos as seguintes funções.
def soma(l_inf, l_sup): resultado = 0 for x in range(l_inf, l_sup + 1): resultado += x return resultado def soma_quadrado(l_inf, l_sup): resultado = 0 for x in range(l_inf, l_sup + 1): resultado += x*x return resultado
Qual é o padrão comum? Será que podemos abstrair esse padrão comum?
Na realidade é fácil observar que estas duas funções diferem apenas na forma como o termo do somatório é calculado, ou seja, a função $f$ em \[ \sum_{n = l_{inf}}^{l_{sup}} f(n) \] Logo, uma vez que as funções em Python, tal como nas linguagens puramente funcionais, são entidades de primeira ordem, podemos passara $f$ por parâmetro,
def somatorio(l_inf, l_sup, f): resultado = 0 for x in range(l_inf, l_sup + 1): resultado += f(x) return resultado
Agora podemos calcular o somatório entre dois limites com um qualquer termo genérico:
>>> def quadrado(x): ... return x*x ... >>> somatorio(1,10,quadrado) 385
Na realidade podemos ainda ter algo mais abstracto. Em geral podemos crer avançar o somatório com um passo diferente de somar uma unidade. Nesse caso podemos definir algo como:
def somatorio(l_inf, l_sup, f, passo): resultado = 0 while l_inf <= l_sup: resultado += f(l_inf) l_inf = passo(l_inf) return resultado
As funções que utilizam funções com argumentos, como acontece com a função somatorio
, dizem-se funcionais ou funções de ordem superior.
Na realidade o que fizemos foi passar nomes de funções à função somatorio
. Como vimos antes os nomes servem apenas para nomearmos as abstracções, o que nos interessa é o algoritmo abstraído pela função, ou seja, o saber como calcular. Por outro lado a nomeação de uma função serve muitas vezes apenas o propósito de passar esse nome por parâmetro, tal como aconteceu com o quadrado
, não sendo relevante fora desse contexto. Será possível nestes casos passar uma função anónima?
O matemático Alonzo Church inventou em 1941 o cálculo lambda, o qual já tínhamos referido numa aula anterior quando falámos de programação funcional. O cálculo lambda permite-nos modelar funções, e.g., $\lambda x. x+3$ que corresponde à função que dado o argumento $x$ toma o valor $x+3$. Para avaliar uma função em cálculo lambda escreve-se em geral $(\lambda x. x+3) 3$, que tem o valor $6$. Embora pudéssemos dizer muito mais sobre o cálculo lambda e aquilo que nos permite modelar, diremos para já que o cálculo lambda é um modelo de computação universal e que serviu de inspiração a várias linguagens de programação. Neste âmbito é importante salientar que no cálculo lambda as funções não são em geral nomeadas e que em muitas linguagens, em particular em Python, existe a possibilidade de definir funções anónimas recorrendo precisamente a uma notação inspirada no cálculo lambda. No que diz respeito a Python, em BNF
<função anónima> ::= lambda <parâmetros formais>: <expressão>
Exemplos:
>>> (lambda x: x+3)(3) 6 >>> (lambda x: 2*x if x%2 != 0 else x)(5) 10 >>> somatorio(1, 10, lambda x : x*x, lambda x : x + 1) 385 >>>
Notar que uma expressão lambda não inclui return
, o valor da expressão aplicada a argumentos concretos é o valor da expressão associada à expressão lambda.
Vejamos um outro exemplo em que se mostra que funções que recebem outras funções como argumentos permitem definir métodos gerais de computação, independentes das funções envolvidas. Consideremos o método da bissecção, que tem por base o teorema de Bolzano.
def metodo_bisseccao(f, a, b): if a > b: raise ValueError("metodo_bisseccao: a > b!?") def aproxima_raiz(f, a, b): while not abs(a - b) < 0.0001: m = a + (b - a) * 0.5 if f(m) > 0: b = m elif f(m) < 0: a = m else: return m return a + (b - a) * 0.5 x = f(a) y = f(b) if x < 0 < y: return aproxima_raiz(f, a, b) elif y < 0 < x: return aproxima_raiz(f, b, a) else: raise ValueError("metodo_bisseccao: sig(f(a)) == sig(f(b))!?")
Exemplo de utilização:
>>> metodo_bisseccao(lambda x: x*x - 2, 0, 1) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 24, in metodo_bisseccao ValueError: metodo_bisseccao: sig(f(a)) == sig(f(b))!? >>> metodo_bisseccao(lambda x: x*x - 2, 1, 2) 1.414215087890625 >>> import math >>> math.sqrt(2) 1.4142135623730951 >>>
Funções como valor de funções
Acabámos de ver que podemos utilizar funções como parâmetros. Veremos agora que as funções também podem produzir/retornar valores que são funções.
Consideremos o cálculo da derivada de uma função de variável real $f$. Por definição, \[ f'(a) = \lim_{x\rightarrow a} \frac{f(x) - f(a)}{x - a} \] Substituindo $h = x -a$, \[ f'(a) = \lim_{h\rightarrow 0} \frac{f(a + h) - f(a)}{h} \] Se $dx$ for um número suficientemente pequeno, podemos considerar a seguinte aproximação: \[ f'(a) \approx \frac{f(a + dx) - f(a)}{dx} \]
Podemos portanto definir:
def derivada_num_ponto(f, a): dx = 0.00001 return (f(a + dx) - f(a)) / dx
Exemplo de utilização:
>>> derivada_num_ponto(lambda x : x*x, 3) 6.000009999951316 >>>
Podemos no entanto definir função derivada de $f$ da seguinte forma:
def derivada(f): dx = 0.00001 def derivada_num_ponto(x): return (f(x + dx) - f(x)) / dx return derivada_num_ponto
Exemplo de utilização:
>>> g = derivada(lambda x: x*x - 2) >>> g(3) 6.000009999951316 >>> g(4) 8.00000999952033
Em alternativa podemos também definir a derivada de $f$ como:
def derivada(f): dx = 0.00001 return lambda x : (f(x + dx) - f(x)) / dx