HomePage RecentChanges

Lesson13

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