Ir para o conteúdo

Definição Local de Funções

Frequentemente em nossas funções precisamos usar mais de uma vez um valor calculado. Na função a seguir, por exemplo, que indica o nível do índice de massa corporal, o cálculo do índice p / a^2 é feito em três locais diferentes.

imc p a
    | p/a^2 <= 18.5 = "Baixo"
    | p/a^2 <= 25.0 = "Normal"
    | p/a^2 <= 30.0 = "Alto"

Uma alternativa que tornaria o código mais manutenível seria especificar o cálculo em uma função auxiliar, como no seguinte código.

aux p a = p/a^2

imc p a
    | aux p a <= 18.5 = "Baixo"
    | aux p a <= 25.0 = "Normal"
    | aux p a <= 30.0 = "Alto"

Entretanto, a aux é visível para todas as outras funções especificadas no mesmo arquivo, mesmo que não tenham nenhum interesse na mesma. Além disso, se outras funções auxiliares forem necessárias, terão cada uma que usar um nome único. Para estes casos, Haskell nos permite fazer definições "locais" das funções auxiliares, isto é, que serão visíveis apenas dentro do escopo da função em que são definidas. Isso pode ser feito usando-se where e let in.

where

As definições usando where são feitas após o bloco em que estas definições são usadas, como no seguinte exemplo.

imc p a
    | imc' p a <= 18.5 = "Baixo"
    | imc' p a <= 25.0 = "Normal"
    | imc' p a <= 30.0 = "Alto"
    where imc' p' a' = p'/a'^2

Esta definição pode ser simplificada usando o fato de que todos os parâmetros formais da função externa são também visíveis dentro da função interna.

imc p a
    | imc' <= 18.5 = "Baixo"
    | imc' <= 25.0 = "Normal"
    | imc' <= 30.0 = "Alto"
    where imc' = p/a^2

Outro aspecto importante do uso de where é que várias definições podem ser feitas, bastando que todas estejam perfeitamente indentadas. No exemplo a seguir, fica fácil localizar e modificar as definições dos vários níveis de IMC. Ponto para a manutenabilidade!

imc p a
    | imc' <= baixo = "Baixo"
    | imc' <= normal = "Normal"
    | imc' <= alto = "Alto"
    where imc' = p/a^2
          baixo = 18.5
          normal = 25.0
          alto = 30.0

where é especialmente útil na definição de funções recursivas, pois permite nomear as invocações, como por exemplo na definição da função que calcula os termos da sequência de Fibonacci.

fib 0 = 0
fib 1 = 1
fib n = fib (n - 1) + fib (n - 2)
fib 0 = 0
fib 1 = 1
fib n = prev + prevPrev
    where prev     = fib (n - 1) 
          prevPrev = fib (n - 2)

Neste caso pode-se argumentar que não houve ganhos com o mudança, dado que a função é muito simples. Em funções mais complexas, contudo, os benefícios em termos de legibilidade aumentam, como na função a seguir.

collatz :: Int -> Int -> Int
collatz n i
    | i == 1 = n
    | even anterior = anterior `div` 2
    | otherwise = anterior * 3 + 1
    where anterior = collatz n (i-1)

converge :: Int -> Int -> Bool
converge n passo
    | termo == 1 = True
    | otherwise = converge n (passo + 1)
    where termo = collatz n passo

Finalmente, note que where pode ser aninhado, isto é, definições locais podem também ter suas próprias definições locais.

let ... in

let ... in oferece uma outra alternativa em Haskell para fazer definições locais. Neste caso, definições que se seguem ao let são visíveis na expressão definida no in.

fib 0 = 0
fib 1 = 1
fib n = let prev     = fib (n - 1) 
            prevPrev = fib (n - 2)
        in prev + prevPrev

areaCilindro r a = 
    let areaLateral = 2 * pi * r * a
        areaTopo = pi * r^2  
    in  areaLateral + 2 * areaTopo

Nestes exemplos, parece que where e let in são apenas sintaxes diferentes para o mesmo fim, criar definições locais, mas há algumas diferenças fundamentais. Primeiro, enquanto o as definições feitas no where são amarradas ao contexto da função em que estão colocadas, podendo ser vistas em quaisquer outras partes da função, por exemplo nas guardas do cálculo do imc, as definições feitas no let são muito mais localizadas, válidas apenas dentro do in correspondente. Segundo, let ... in são expressões e podem aparecer em qualquer lugar onde uma expressão é esperada. Por exemplo, eles podem ser usados para definir constantes ou funções literalmente no meio de uma expressão maior.

> 2 * (let um = 1 in um + 2) + 3
9
> let quadrado x = x*x in quadrado 3
9

Em alguns contextos em que o escopo é muito claro, podemos omitir o in da definição. No GHCi, por exemplo, se o fizermos então a definição feita será válida por toda a execução do REPL, ou até que seja sobrescrita.

> let quadrado x = x*x in quadrado 3
9
> quadrado 3

<interactive>:25:1: error:
    Variable not in scope: quadrado :: t0 -> t
> let quadrado x = x*x
> quadrado 3
9