Saltar para o conteúdo

Haskell/Variáveis e funções

Origem: Wikilivros, livros abertos por um mundo aberto.


Este módulo encontra-se em processo de tradução. A sua ajuda é bem vinda.

Todos os exemplos deste capítulo pode ser salvos num arquivo e executados usando o GHC. Não inclua "Prelude>" em nenhum dos exemplos. Quando este comando aparecer, quer dizer que o código seguinte foi rodado no GHCi. Quando não houver o comando, basta copiar o código inteiro.

No capítulo passado, usamos o GHCi como uma calculadora, o que é tão prático apenas para contas curtas. Para grandes cálculos e para escrever um programa em Haskell, geralmente temos que acompanhar e verificar resultados intermediários.

Podemos fazer isso atribuindo nomes. Chamas as chamadas variáveis. Quando um programa é executado, cada variável é substituída por um valor ao qual ela se refere. Por exemplo, considere o seguinte

Prelude> 3.141592653 * 5^2
78.539816325

Este foi o cálculo aproximado da área de um círculo de raio 5, de acordo com a fórmula . Claro que se torna um processo bastante trabalhoso escrever todos os dígitos de , ou mesmo lembrar os primeiros 5. Por isso, para evitar repetições podemos fazer com a máquina se lembre destes valores uma única vez para que possamos usá-los sempre que necessário. Essa abstração nos deixa livre para focar em outras partes mais importantes. No caso deste exemplo, Haskell já possui uma variável chamada pi que armazena mais de 15 dígitos de , o que deixa o código mais claro e ainda ganhamos em precisão nos resultados.

Prelude> pi
3.141592653589793
Prelude> pi * 5^2
78.53981633974483

Perceba que a variável pi e seu valor, 3.141592653589793, trocados entre si nos cálculos.

Arquivos de código em Haskell

[editar | editar código-fonte]

Além de fazer cálculos esporadicamente no GHCi, você pode salvar seus códigos num arquivo Haskell, que são arquivos de texto com a extensão .hs. É recomendável usar um editor de texto próprio para programação (cf. artigo da Wikipédia sobre editores de texto). A grande maioria deles possuem a capacidade realçar a sintaxe, isto é, colorir o código fonte de modo a facilitar a leitura. Vim e Emacs são duas opções bastante populares entre programadores.

Para ajudar na organização, crie também um novo diretório (uma pasta de arquivos) em seu computador para salvar os arquivos Haskell que você cria quando estiver resolvendo os exercícios deste livro. Vamos lá, crie um diretório chamado HaskellWikibook, por exemplo, e dentro dele, um arquivo chamado Varfun.hs contendo o seguinte código:

r = 5.0

Este programa define a variável r como sendo o valor 5.0.

Nota: certifique-se de que não há espaços em branco no começo das linhas, porque Haskell é sensível a espaços.

Depois abra um terminal no diretório HaskellWikibook que você criou, execute o GHCi e carregue o arquivo Varfun.hs usando o comando :load:

Prelude> :load Varfun.hs
[1 of 1] Compiling Main             ( Varfun.hs, interpreted )
Ok, modules loaded: Main.

É bom saber que :load pode ser abreviado para :l: :l Varfun.hs.

Se o GHCi retornar um erro como Could not find module 'Varfun.hs', isso quer dizer que você provavelmente está no diretório errado. Tente o comando :cd para mudar o diretório corrente de dentro do GHCi (por exemplo, :cd HaskellWikibook).

Depois que o arquivo estiver carregado na sessão do GHCi, você vai ver a linha de comando mudar de "Prelude" para "*Main". Agora você pode usar a varíavel r que você definiu no arquivo.

*Main> r
5.0
*Main> pi * r^2
78.53981633974483

Acabamos de calcular a área de um círculo de raio 5,0 usando a fórmula . O que aconteceu aqui é que usamos r que definimos no arquivo Varfun.hs, e pi, que já está definido em outro lugar.

Agora, vamos fazer esse valor ser mais fácil de ser acessado ao definirmos um nome para ele também. Abra o arquivo novamente e mude seu conteúdo para:

r = 5.0
area = pi * r ^ 2

Salve-e e recarregue-o no GHCi usando :reload, ou simplesmente :r:

*Main> :reload
Compiling Main             ( Varfun.hs, interpreted )
Ok, modules loaded: Main.
*Main>

Agora temos duas variávels: r e area.

*Main> area
78.53981633974483
*Main> area / r
15.707963267948966
Nota: A palavra let é o que deveríamos usar para definir novas variáveis diretamente no GHCi, sem precisar editar um arquivo de código. Mesmo que possa ser prático fazer isso, nem sempre é o caso para tarefas mais complexas. É preferível usar arquivos nesses casos.

Além do código em si, arquivos de código fonte podem conter texto conhecidos como cometários, que são ignorados pelo compilador. Em Haskell existem dois tipos de comentários. Primeiro, aqueles que começam com -- e que vão até o final da linha:

x = 5     -- x is 5.
y = 6     -- y is 6.
-- z = 7  -- z is not defined.

Nestes casos, x e y são realmente definidos no código, mas z não é.

O segundo tipo do comentário é deliminado por {- ... -} e podem se abranger mais de uma linha:

answer = 2 * {-
  bloco de cometário de mais de uma linha e...
  -} 3 {- comentário no meio da linha -} * 7

Comentários são usados para explicar partes de um programa ou para fazer qualquer tipo de anotação acerca de algum contexto. É importante frisar que exagerar na quantidade de comentários pode, na verdade, dificultar a leitura do código em si, e comentários desatualizados podem também gerar confusões.

Variáveis em linguagens imperativas

[editar | editar código-fonte]

Os leitores familiares com linguagens de programação imperativa perceberão que as variáveis em Haskell se comportam de maneira bem diferente que as variáveis em C, por exemplo. Se você não possui experiência prévia com programação, pode pular esta seção, apesar de que ela pode te ajudar a entender as situações em que pessoas comparam o comportamento de Haskell em relação a outras linguagens. Muitos livros sobre Haskell fazem isso, por exemplo.

Programação imperativa trata as variáveis como posições na memória da máquina. Essa abordagem está relacionada com o funcionamento básico de um computador. Programas imperativos dizem explicitamente ao computador o que devem fazer. Linguagens de alto nível, que podem estar bastante distantes destes conceitos, ainda retém parte deles no sentido de que ainda usam esse padrão de programação "passo-a-passo". Em contraste a isso, programação funcional proporciona uma maneira diferente de pensar, usando termos matemáticos e deixando de lado as instruções diretas ao computador, que são traduzidas apenas compilador para algo que o computador possa interpretar e processar.

Vejamos um exemplo. O código a seguir não funcionaria em Haskell:

r = 5
r = 2

Um programador de linguagens imperativas leria isto como sendo: primeiro, definir r = 5 e depois redefinir para r = 2. Em Haskell, entretanto, o compilador responderia este código com um erro: "múltiplas declarações de r". Dentro de um escopo definido, uma variável de Haskell é definida uma única vez, e sua definição não pode mudar.

As variáveis de Haskell parecem invariáveis, mas a verdade é que elas se comportam como variáveis matemáticas. Na Matemática, você nunca vê uma variável mudar de valor num mesmo problema, e a mesma coisa acontece em Haskell.

De forma mais precisa, as variáveis de Haskell são imutáveis: elas variam apenas de acordo com os datos que entramos no programa. Não podemos definir r de dois jeitos diferentes num mesmo código, mas podemos mudar este valor se mudarmos de arquivo. Vamos mudar o código acima para:

r = 2.0
area = pi * r ^ 2

Claro que vai funcionar. Mudamos r no único lugar em que ele está definido, e isso muda seu valor em todo o resto do programa.

Em problemas reais, os programas em Haskell deixam alguns valores indefinidos no código. Esses valores são definidos mais tarde, quando o programa adquire dados de um arquivo ou ação externa. Mas por enquanto, vamos nos ater a definir variáveis internamente. Vamos aprender sobre interações com o mundo exterior no capítulos futuros.

Aqui temos mais um exemplo da grande diferença entre Haskell com as linguagens imperativas:

r = r + 1

Em vez de "incrementar o valor de r", isto é, atualizar o valor já armazenado na memória, este código em Haskell é a definição recursiva e r, ou seja, ele foi definido em termos de si mesmo. Não se preocupe, pois explicaremos recurssão mais tarde. Neste caso específico, se r tiver sido definido com algum valor anterior, o compilador retornaria uma mensagem de erro, como explicamos anteriormente. Se r tiver sido definido com um valor de 5, fazer r = r + 1 é o mesmo que tentar dizer que na Matemática, o que está obviamente errado.

Já que os valores não mudam dentro de um mesmo programa, variáveis podem ser definidas na ordem em que quisermos. Por exemplo, os dois códigos a seguir são exatamente a mesma coisa:

 y = x * 2
 x = 3
 x = 3
 y = x * 2

Em Haskell não temos essa noção de que "x foi declarado antes de y", ou o contrário. Claro, para usarmos y ainda precisamos do valor de x, mas a ordem não nos interessam, e nem ao compilador.

Mudar o programa todas as vezes em que quisermos calcular a área de um círculo diferente é tedioso e nos limita a um círculo por vez. Poderíamos calcular duas áreas duplicando todas as contas em nosso código, definindo r2 e area2[nota 1]

r  = 5
area  = pi * r ^ 2
r2 = 3
area2 = pi * r2 ^ 2

Para evitarmos essa repetição incessante, é preferível simplesmente adicionar uma função para calcular a área e aplicá-la a diferentes raios.

Uma função recebe um argumento (ou parâmetro) e retorna um resultado, exatamente como na Matemática. Em Haskell, as funções são definidas da mesma forma que as variáveis, exceto que temos que dizer quais são seus argumentos do lado esquerdo da expressão. Por exemplo, o código a seguir define area, cujo argumento é r:

area r = pi * r ^ 2

Observe a sintáxe: o nome da função vem primeiro (area), seguido de um espaço em branco e de seu argumento (r). Depois vem o sinal =, e a definição da função, que usa o argumento de entrada em seu cálculo.

Agora podemos testar diferentes valores de raio e chamar a função com eles. Salve o código acima num arquivo, abra-o no GHCi, e experimente:

*Main> area 5
78.53981633974483
*Main> area 3
28.274333882308138
*Main> area 17
907.9202768874502

Acabamos de chamar a função com diferentes valores e calculamos a área de três círculos diferentes.

Matematicamente, o calculo da área pode ser representado por . Para calcular as áreas, faríamos ou . Haskell também funciona com parênteses, mas eles são geralmente omitidos por convenção para deixar o código menos carregado de símbolos e mais legível.

Mesmo assim, parênteses ainda são usados para agrupar expressões (qualquer código que retorne algum valor) que devem ser calculadas todas juntas. Veja como as seguintes expressões são interpretadas de formas diferentes:

5 * 3 + 2       -- 15 + 2 = 17 (multiplicação feita antes da adição)
5 * (3 + 2)     -- 5 * 5 = 25 (parenteses forçam a adição a ser computada primeiro)
area 5 * 3      -- (area 5) * 3
area (5 * 3)    -- area 15

Da mesma forma que a multiplicação acontece antes que a adição, perceba que as funções de Haskell tem hierarquia de precedência maior que qualquer outro operador, seja + ou *, por exemplo.

Avaliação de expressões

[editar | editar código-fonte]

O que exatamente acontece quando entramos uma expressão no GHCi? Depois que pressionamos "enter", ela é avaliada. Isso significa que cada função vai ser substituída por sua definição e as contas serão realizadas até que um único valor final reste. Por exemplo, ao executarmos area 5 o seguinte acontece:

area 5                     -- todos 'r' do lado direito serão substituidos por '5'
pi * 5 ^ 2                 -- 'pi' será substituido por um valor aproximado
3.141592653589793 * 5 ^ 2  -- a operação de potenciação '5 ^ 2' será realizada
3.141592653589793 * 25     -- a operação de multiplicação será realizada
78.53981633974483          -- resultado final

Quando usamos o GHCi, o resultado de aplicar ou chamar a função, que segue o procedimento acima, vai aparecer na tela.

Agora, mais algumas funções:

dobrar   x     = 2 * x
quadruplicar x = double (double x)
quadrado x     = x * x
metade   x     = x / 2
Exercícios
  • Explique como GHCi avaliaria quadruplicar 5}.
  • Defina uma função que subtraia 12 da metade de seu argumento.

Parâmetros múltiplos

[editar | editar código-fonte]

Funções também podem ter mais que um argumento. Por exemplo, para calcular a área de um retângulo precisamos de seu comprimento e de sua largura:

areaRetang a b = a * b
*Main> areaRetang 5 10
50

Outro exemplo é a área de um triângulo, :

areaTriang b h = (b * h) / 2
*Main> areaTriang 3 9
13.5

Como você pode ver, os argumentos são separados por espaços. É por isso que as vezes você precisa usar parênteses para agrupar expressões. Por exemplo, não se pode escrever o quádruplo de um valor como sendo

quadruple x = double double x     -- error

Isso aplicaria a função double sobre dois argumentos: double e x. É possível, sim, que funções sejam argumentos de outras funções, como veremos mais tarde, mas isso não é o que queremos aqui. Para fazer esta função funcionar precisamos de parênteses:

quadruple x = double (double x)

Os argumento são sempre passados na ordem em que aparecem. Por exemplo:

menos x y = x - y
*Main> menos 10 5
5
*Main> menos 5 10
-5

Aqui, menos 10 5 avalia a expressão 10 - 5, enquanto que menos 5 10 avalia 5 - 10 devido a mudança na ordem do argumentos.

Exercícios
  • Escreva uma função para calcular o volume de uma caixa.
  • Aproximadamente, de quantas pedras as famosas pirâmides de Gizé feitas? Dica: você vai precisar estimar o volume da pirâmide e o volume de cada bloco de pedra.

Combinando funções

[editar | editar código-fonte]

Claro que você pode usar outras funções para definir suas próprias, bem como você já o fez usando adição, +, ou multiplicação, *. Na verdade, operadores também são definidos como funções em Haskell. Por exemplo, para calcular a área de um quadrado, podemos reutlizar a função que calcula a área de um retângulo:

areaRetang a b = a b
areaQuad   l   = areaRetang l l
*Main> areaQuad 5
25

Afinal de contas, um quadrado é um retângulo de lados iguais.

Exercícios
  • Escreva uma função que calcule o volume de um cilindro. O volume de um cilindro é a área da base, que é um círculo, multiplicada pela altura. Como você já programou a função da área de um círculo neste capítulo, você pode reutilizá-la aqui.

Definindo funções locais

[editar | editar código-fonte]

Quando definimos uma função, às vezes precisamos calcular valores intermediários, mas que são locais e usados apenas dentro daquela função. Considere a fórmula de Herão: , sendo o metade do perímetro do triângulo. Este é o cálculo da área de um triângulo de lados a, b e c:

herao a b c = sqrt (s * (s - a) * (s - b) * (s - c))
    where
    s = (a + b + c) / 2

A variável s representa a metade do perímetro. Seria tedioso e propenso a erros se tivéssemos que escrever (a + b + c)/2 toda vez que precisássemos deste valor dentro de herao. sqrt calcula a raiz quadrada do seu argumento (lembre-se de square root, no inglês).

Simplesmente escrever as definições separadamente não funcionaria. Veja:

herao a b c = sqrt (p * (p - a) * (p - b) * (p - c))
p = (a + b + c) / 2                                   -- a, b, and c are not defined here

O compilador reclamaria que a, b e c só estão disponíveis no lado direito de herao, enquanto que a definição de s não faz parte do lado direito de herao. Para que ela seja embutida lá, usamos a palavra-chave where.

Perceba que tanto where quanto a definição de s estão indentadas por 4 espaços em branco. Isso é importante para delimitar o que está dentro do que. Vejamos outros exemplos do uso de where:

areaTriangTrig  a b c = c * altura / 2   -- usando trigonometria
    where
    cosa    = (b ^ 2 + c ^ 2 - a ^ 2) / (2 * b * c)
    sina    = sqrt (1 - cosa ^ 2)
    altura  = b * sina
areaTriangHerao a b c = resultado        -- usa a fórmula de Herão
    where
    resulto = sqrt (p * (p - a) * (p - b) * (p - c))
    p       = (a + b + c) / 2

Observe atentamente o exemplo anterior: perceba que usamos as variáveis a, b, c duas vezes, em cada função definida. Como pode isso?

Considere o seguinte procedimento no GHCi:

Prelude> let r = 0
Prelude> let area r = pi * r ^ 2
Prelude> area 5
78.53981633974483

Seria estranho se retornasse 0 para as áreas por causa da definição anterior de let r = 0. Isso não acontece porque na segunta vez que digitamos r estamos nos referindo a um r diferente. Parece confuso, mas pense no seguinte: muitas pessoas se chamam João, mesmo assim, numa situação em que há apenas um João presente, podemos falar "João" sem nenhuma confusão. Em programação, essa noção de contexto é chamada de escopo.

Não vamos nos aprofundar nos detalhes técnicos que envolvem o escope por agora, mas tenha em mente que o valor de um parâmetro é exatamente aquilo que você passa à função, independente de como você o chamou na definição da função. Assim sendo, nomear as variáveis de modo apropriado ajuda bastante a leitura do código por humanos.

  1. Variáveis armazenam valores (que podem ser qualquer expressão em Haskell).
  2. Variáveis não mudam seus valores dentro de um mesmo escopo.
  3. Funções são úteis para escrever programas reutilizáveis.
  4. Funções podem aceitar mais de um parâmetro.
  1. Podemos ver aqui que os nomes de variáveis podem contar números e letras. Entretanto, toda variável deve começar com um letra minúscula, que pode ser seguida por outras letras (também maiúsculas), números, traço (_) ou apóstrofe (').