0% acharam este documento útil (0 voto)
30 visualizações34 páginas

Introdução à Programação Funcional

jjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjj
Direitos autorais
© All Rights Reserved
Levamos muito a sério os direitos de conteúdo. Se você suspeita que este conteúdo é seu, reivindique-o aqui.
Formatos disponíveis
Baixe no formato PDF, TXT ou leia on-line no Scribd
0% acharam este documento útil (0 voto)
30 visualizações34 páginas

Introdução à Programação Funcional

jjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjj
Direitos autorais
© All Rights Reserved
Levamos muito a sério os direitos de conteúdo. Se você suspeita que este conteúdo é seu, reivindique-o aqui.
Formatos disponíveis
Baixe no formato PDF, TXT ou leia on-line no Scribd

Este trabalho está licenciado com uma Licença Creative Commons

Atribuição-NãoComercial-SemDerivações 4.0 Internacional.


TÉCNICAS DA PROGRAMAÇÃO FUNCIONAL

Sumário
1. Motivações para lambda calculus

2. Funções anônimas e expressões lambda, introdução


às funções recursivas e recursividade em cauda

3. Módulos e funções de alta ordem e imutabilidade e


efeitos colaterais

4. Organização e manipulação de dados imutáveis:


listas, tuplas e maps

Sumário clicável
Voltar ao sumário
A programação funcional é um paradigma que usa Olá
funções e expressões matemáticas para resolver
problemas computacionais. Destaca-se pela
imutabilidade dos dados, priorizando funções como
elementos centrais. O Cálculo Lambda de Alonzo
Church é fundamental nesse paradigma. Conceitos-
chave são funções anônimas, expressões lambda,
recursividade e modularidade. A programação
funcional usa estruturas de dados imutáveis, como
listas, tuplas e maps. Proporciona flexibilidade na
composição de funções e reutilização de código. A
imutabilidade garante previsibilidade e facilidade de
teste. Resumindo, a programação funcional oferece
uma abordagem poderosa, baseada em funções,
expressões matemáticas, imutabilidade de dados e
técnicas como recursividade e modularidade.

4
1.

Voltar ao sumário
Motivações para lambda calculus

Neste Percurso de Aprendizagem, nós vamos aprender sobre calculo λ (lambda),


apresentando o seu contexto histórico bem como a sua importância para a programação
de linguagens funcionais. Vamos lá!

Em meados de 1930, existia uma forte discussão acadêmica acerca da definição formal
de Computabilidade, pois, intuitivamente, algumas coisas são altamente computáveis, ou
seja, são facilmente resolvíveis através da aplicação de um algoritmo de passos finitos,
como, por exemplo: o cálculo de uma raiz quadrada; o cálculo do inverso de uma String,
a soma de dois números, a verificação se um número é primo, ou se o número é positivo,
dentre outros, porém uma pergunta precisava ser respondida: Qual é o conjunto de todas
as coisas que são computáveis?

Informalmente, podemos definir Computabilidade como uma área de estudo central da


Ciência da Computação, que estuda a possibilidade de resolver problemas a partir de
algoritmos. Por essa definição, podemos perceber que quase tudo que é feito dentro
de um computador é considerado computável, porém temos coisas não tão simples de
resolver e/ou entender, uma delas é a concorrência (quando um sistema vai parar sua
execução para esperar por outro).

Em 1936, Alan Turing publicou On computable numbers, with an application to the


entscheidungsproblem, apresentando a criação de uma máquina automática que era
uma Máquina de Estado Finita cujas entradas eram realizadas a partir de uma fita de
execução de tamanho arbitrário. Esta máquina conseguia ler, escrever e navegar por essa
fita, avançando e retrocedendo, quando necessário.

No mesmo ano Alonzo Church publicou seu artigo “An unsolvable problem of elementary
number theory”, apresentando um sistema formal para expressar computação baseado em
abstrações de funções e aplicação usando apenas atribuições de nomes e substituições.
Esse sistema ficou conhecido como cálculo λ. Ambos os artigos concluíram que o
problema enfrentado era de decisão e, como resultado, concluíram que esse problema
não era computável. Ainda em 1936, Turing, após conhecer o artigo de Church, anexa um
apêndice ao seu artigo, afirmando que o seu conceito de computabilidade era semelhante
ao conceito de efetivamente calculável.

Atualmente, a definição de computável é definida como qualquer problema que possa ser
resolvido pelo cálculo λ ou em uma máquina de estados finita de Turing. Cientes disso,
podemos analisar quais são os tipos de problemas associados a essa solução:

• Decisão: verifica se um elemento também está contido em T.


Exemplo: testar se um número é primo, .

5
Voltar ao sumário
• Função: calcular o resultado da aplicação de uma função:
Exemplo: inverter uma String.
• Busca: verificar se xRy em uma relação binária R.
Exemplo: buscar um nó em um grafo.
• Otimização: encontrar a solução x* entre todas as soluções do espaço de busca S
de tal forma a maximizar ou minimizar uma função f(x).
Exemplo: quanto devo colocar em cada possível investimento para maximizar
meus lucros.

Note que todo esse debate acaba levantando outro questionamento interessante: Qual é
a menor linguagem universal possível? Nas linguagens que você estudou até agora, no
curso de Análise e Desenvolvimento de Sistemas, todas elas apresentam:

Análise e
Desenvolvimento
de Sistemas

Porém, existe uma linguagem que somente utiliza funções: o cálculo λ, tendo sua
linguagem descrita em função de sua sintaxe e semântica. Sua Sintaxe é composto por
3 elementos:

• Variáveis: são símbolos usados para representar valores que podem variar. Elas
são usadas para expressar relações matemáticas e permitir o cálculo de funções
em diferentes pontos. Por exemplo, em uma função f(x), a variável x representa um
valor que pode ser substituído por um número real para obter o valor correspondente
da função.

6
Voltar ao sumário
• Definição de funções: especifica a relação entre uma variável independente e uma
variável dependente. Em geral, uma função é representada por uma expressão
matemática que relaciona a variável independente (geralmente denotada por x)
com a variável dependente (geralmente denotada por f(x) ou y). Por exemplo, a
função f(x) = 3x é uma definição de função que associa a cada valor de x o triplo
desse valor.
• Aplicação de funções: envolve a substituição de um valor específico na variável
independente para determinar o valor correspondente na variável dependente. Isso
significa substituir o valor dado para a variável x na expressão da função e calcular
o valor resultante. Por exemplo, se tivermos a função f(x) = 3x e quisermos calcular
o valor de f(3), devemos substituir x por 3 na função: f(3) = 3 * 3 = 9. Portanto, a
aplicação da função f(x) = 3x para x = 3 resulta em f(3) = 9.

Na linguagem Haskell, o simbolo λ é substituido por \, enquanto que o símbolo . é


substituído por →, conforme podemos observar no exemplo abaixo:

e :: = x
| \x -> e
| e1 e2

Um programa é definido por uma expressão e, ou termos -λ que podem assumir uma de
três formas:

Variável: x, y, z, um nome que assumirá um valor durante a computação.

Abstração: (ou função anônima ou função λ)

\x -> e,
//x é o parâmetro formal, e é o corpo da função
para qualquer valor x compute e

Aplicação: (ou chamada de função)

e1 e2, aplique o argumento e2 na função e1 (e1(e2)).

Todo ei é uma expressão! Exemplos:

λx -> x //função identidade


λx -> (λy -> y) //retorna a função identidade
λf -> f (λx -> x) //função que aplica seu argumento, que é uma
função na função identidade

7
Voltar ao sumário
Funções de dois ou mais argumentos

\x -> (\y -> y) //recebe dois args e retorna o segundo


\x -> (\y -> x) //recebe dois args e retorna o primeiro

Na programação existe um termo chamado Syntactic Sugar, utilizado para descrever


uma característica da linguagem que não adiciona funcionalidade nova, mas oferece
uma sintaxe mais conveniente, legível e expressiva para escrever código.

Quando dizemos que algo é um Syntactic Sugar, estamos nos referindo a uma construção
ou uma notação que permite escrever um determinado trecho de código de forma mais
curta ou com uma sintaxe mais familiar, sem alterar o comportamento ou a semântica
subjacente do código. Essa notação mais amigável e concisa facilita a compreensão do
código e reduz a quantidade de código necessário para alcançar o mesmo resultado, ou
seja, é uma maneira de tornar a linguagem de programação mais expressiva, evitando
que o programador precise escrever código mais prolixo e complexo para realizar tarefas
comuns. A seguir, apresentaremos alguns exemplos de Syntactic Sugar encontrados em
várias linguagens de programação:

• Operadores de atribuição combinados: Muitas linguagens permitem combinar o


operador de atribuição “=” com outros operadores, como “+=”, “-=”, “*=”, etc. Em vez
de escrever “n = n + 10”, pode-se simplesmente escrever “n += 10”.
• Iteração simplificada: Algumas linguagens oferecem construções mais concisas
para iteração, como o “for-each”, que permite percorrer uma coleção de elementos
sem a necessidade de controlar explicitamente o índice ou o contador.
• Sintaxe de string interpolada: Em vez de concatenar strings com operadores
ou funções específicas, algumas linguagens permitem incorporar variáveis
diretamente em strings usando uma sintaxe especial, como o uso de “$” ou “${}”.
• Funções lambda: Linguagens de programação funcional, como Python , JavaScript
e Haskell, permitem definir funções anônimas de forma mais compacta usando a
sintaxe de funções lambda, evitando a necessidade de criar funções separadas,
por exemplo:
Quadro 1 - Syntatic Sugar

Original Syntatic Sugar

(((e1 e2) e3) e4) e1 e2 e3 e4

\x -> (\y -> (\z -> e)) \x -> \y -> \z -> e

\x -> \y -> \z -> e \x y z -> e

Fonte: elaborado pelo autor (2023)

Outro tema interessante que deve ser abordado por quem está iniciando seus estudos
em programação funcional é o escopo de uma variável. O escopo indica a visibilidade de

8
Voltar ao sumário
uma variável em uma determinada linguagem de programação, C ou Java, por exemplo.

int x=10; //x está visível aqui, mas y ainda não foi criado neste
ponto do código
if (x>5){
int y; //x e y estão visíveis neste ponto do código
}
//x está visível aqui, mas y não está visível neste ponto do código,
pois deixou de existir.

Na expressão

\x -> e

x é uma variável, de modo que a expressão e é o escopo de x.

Qualquer ocorrência de x em e está ligada (bound) por \x:

\x -> x
\x -> \y -> x

Por outro lado, x está livre (free) se não está dentro de uma abstração. Para finalizar
o aspecto sintático do cálculo lambda, abordaremos as expressões fechadas, também
conhecidas como expressões lambda, funções anônimas ou combinadoras, que são uma
construção fundamental. Elas permitem definir funções de forma concisa e expressiva,
sem a necessidade de criar uma função nomeada separada.

Uma expressão fechada é uma função sem nome que pode ser definida inline, no ponto em
que será usada, e geralmente é usada como um valor ou argumento de outra função. Elas
são chamadas de “fechadas” porque são autossuficientes e encapsulam seu ambiente,
incluindo as variáveis e constantes às quais têm acesso.

Uma expressão fechada normalmente consiste de três partes principais:

• Parâmetros: É a lista de parâmetros que a função recebe. São as entradas para a


função e podem ser zero ou mais parâmetros. Por exemplo, uma expressão fechada
que recebe um único parâmetro pode ter a forma (x) => ..., onde x é o parâmetro.
• Corpo: É a implementação da função. Pode ser uma única expressão ou um bloco
de código entre chaves. O corpo define as operações a serem realizadas com os
parâmetros e retorna um resultado. Por exemplo, a expressão fechada (x) => x * 2
multiplica o parâmetro x por 2 e retorna o resultado.
• Valor de retorno: É o resultado produzido pela função. Uma expressão fechada
geralmente retorna um valor calculado a partir dos parâmetros e do corpo da função.
O valor de retorno pode ser implícito, quando o resultado é a própria expressão
avaliada, ou pode ser explicitamente especificado usando a palavra-chave return.

9
Voltar ao sumário
As expressões fechadas são especialmente úteis quando se trata de passar funções
como argumentos para outras funções ou quando se deseja criar funções de maneira
mais concisa. Elas permitem a criação de código mais modular, expressivo e legível,
evitando a necessidade de criar funções nomeadas separadas.

A seguir, apresentamos um exemplo de uma expressão fechada que recebe um array


de números e retorna um novo array com os números multiplicados por 2 em diversas
linguagens funcionais:

JavaScript

const numbers = [1, 2, 3, 4, 5];


const doubledNumbers = [Link]((x) => x * 2);
[Link](doubledNumbers);

Haskell

main :: IO ()
main = do
let numbers = [1, 2, 3, 4, 5]
doubledNumbers = map (\x -> x * 2) numbers
print doubledNumbers

Elixir

numbers = [1, 2, 3, 4, 5]
doubled_numbers = [Link](numbers, fn x -> x * 2 end)
[Link](doubled_numbers)

Erlang

-module(main).
-export([start/0]).
start() ->
Numbers = [1, 2, 3, 4, 5],
DoubledNumbers = lists:map(fun(X) -> X * 2 end, Numbers),
io:format(“~p~n”, [DoubledNumbers]).

10
Voltar ao sumário
Python

numbers = [1, 2, 3, 4, 5]
doubled_numbers = list(map(lambda x: x * 2, numbers))
print(doubled_numbers)

Scala

val numbers = List(1, 2, 3, 4, 5)


val doubledNumbers = [Link](x => x * 2)
println(doubledNumbers)

F#

let numbers = [1; 2; 3; 4; 5]


let doubledNumbers = [Link] (fun x -> x * 2) numbers
printfn “%A” doubledNumbers

No próximo Circuito, vamos aprender sobre funções anônimas e expressões Lambda,


fazendo uma introdução às funções recursivas e recursividade em cauda. Até já!

11
2.

Voltar ao sumário
Funções anônimas e expressões Lambda: introdução às
funções recursivas e recursividade em cauda

Olá! Este Circuito apresenta os conceitos essenciais da programação funcional, a qual


tem um paradigma de programação, cujo objetivo é imitar, ao máximo, as funções
matemáticas, que mapeiam entradas (domínio) e saídas (imagens). Uma de suas
características fundamentais é que não existe a noção de estado e, consequentemente,
não utilizamos comandos de atribuição.

O código funcional possui a característica de transparência referencial, ou seja, o valor


da saída de uma função depende apenas dos argumentos passados para ela, o que
quer dizer que centenas de chamadas de uma função realizadas com o mesmo valor de
argumento x sempre retornarão o mesmo resultado f(x).

Isso a difere de outros paradigmas, cujo estado local ou estado global de um programa
podem influenciar no resultado da chamada de uma função.

Uma função matemática típica, como o quadrado de um número:

Quadrado(n) = n * n

Essa definição dá o nome da função (Quadrado), em seguida seus argumentos são


apresentados entre parêntesis (n), e uma expressão que define o seu significado (n*n). Os
argumentos da função são provenientes de um conjunto chamado “Domínio”, enquanto
os valores possíveis de saída formam o conjunto “Imagem” (ou contradomínio).

Uma função matemática é uma relação de um conjunto A com um conjunto B, realizando


o mapeamento dos dados do conjunto domínio para o conjunto imagem, de acordo com
a sua definição. Conforme podemos observar na figura a seguir.
Figura 1 - Definição de uma função

f: A -> B, y = f(x)

Fonte: elaborado pelo autor (2023)

12
Dado o exemplo acima, já podemos perceber que existe uma diferença de caráter

Voltar ao sumário
semântico no uso de sinal de igual, se compararmos a programação imperativa, por
exemplo. Na programação imperativa, o igual significa atribuição, ou seja, atualização de
uma variável de memória (lado esquerdo da igualdade) com um novo valor (lado direito
da igualdade) e mudança do estado geral do programa (pois houve alteração de valores
de variáveis)

x = x + 1 (X recebe o valor de x + 1)

Na programação funcional, a igualdade significa equivalência, as variáveis representam


expressões reais e imutáveis:

Quadrado num = num * num

Como visto, o trabalho de Alonzo Church define a notação lambda, fornecendo um método
para definir funções não nomeadas. Uma expressão lambda especifica os parâmetros e
o mapeamento de uma função. Ela é a função propriamente dita, mas não nomeada. A
seguinte expressão define uma função que eleva um número ao quadrado:

(λx.x*x) //lambda de x é x * x

Antes de ser avaliada, a expressão vale para qualquer membro do conjunto domínio. No
entanto, após avaliada para um determinado parâmetro, ela é aplicada e resulta em um
valor. Exemplo e aplicação que resultam no valor 16:

((λx.x*x) 4) //aplicando 4 a x – teremos que lambda de 4 = 4 * 4


= 16

Logo, as linguagens funcionais são baseadas no framework teórico do cálculo lambda


e fornecem um conjunto de funções primitivas, um conjunto de formas funcionais para
construir funções complexas a partir de aplicação de função e alguma estrutura para
representar os dados.

Em Haskell, uma expressão lambda representando uma função sempre recebe um nome.
Associado a esse nome, é indicado o tipo da função, ou seja, os tipos respectivamente dos
elementos do domínio (Argumentos) e imagem (Retorno) da função. A função “exemplo”
abaixo recebe um inteiro e responde com um outro inteiro, e a expressão lambda é
declarada de maneira muito semelhante à sua forma teórica λx.(x2 + 2x + 5).

//função que calcula uma expressão de segundo grau


exemplo :: Int -> Int
exemplo x = x*x + 2*x + 5

Dentro da linguagem Haskell, temos alguns detalhes que valem ser lembrados, um deles
nos lembra que nomes de funções e parâmetros sempre começam com letra minúscula,
enquanto que tipos começam com letra maiúscula. Não existem parênteses separando
o parâmetro do nome da função (conforme conversamos quando abordamos syntatic
sugar).

13
Voltar ao sumário
A declaração de tipo no Haskell é fortemente tipada, assegurando que a função seja
aplicada a argumentos dos tipos corretos ou esperados. Os tipos básicos da linguagem
incluem inteiros (Int e Integer), reais (Float, Double), booleanos (Bool) e caracteres (Char,
String). Nesta declaração, o último tipo mencionado é sempre o tipo do retorno, enquanto
que os demais tipos são os tipos dos parâmetros, em ordem, conforme podemos observar
na Figura 2 apresentada abaixo:
Figura 2 - Declaração de função

nome argumentos retorno

Fonte: elaborada pelo autor (2023)

Abaixo podemos conhecer as funções fornecidas pela Linguagem:


Quadro 1 – Linguagem

Função Definição

abs Retorna o valor absoluto de um número

ceiling
abs Retorna o menor inteiro não menor que o argumento

cos
ceiling Retorna o cosseno de um número descrito em radianos

div
cos Retorna o quociente inteiro da divisão de dois inteiros

sqrt
div Retorna a raiz quadrada do argumento

truncate
sqrt Retorna o inteiro sem a parte fracionária do número

truncate
Fonte: elaborada pelo autor (2023)

Quadro 2 – Operadores funcionais

> a > b A maior que b

>= a >= b A maior ou igual a b

< a < b A menor que b

<= a <= b A menor ou igual a b

== a == b A igual a b

/= a /= b A diferente de b

Fonte: elaborada pelo autor (2023)

14
Voltar ao sumário
Quadro 3 – Operadores Aritméticos

+ a + b Soma

- a - b Subtração

* a * b Multiplicação

/ a / b Divisão

^ a ^ b Potência

div a div b Divisão inteira

mod a mod b Resto

Fonte: elaborado pelo autor (2023)

Uma função também pode incluir estruturas condicionais, para desviar o fluxo do programa
para diferentes partes, dependendo de uma avaliação da condição (verdadeira ou falsa).
Em Haskell, podemos implementar essas estruturas de acordo com o exemplo abaixo:

//Utilizando condicionais em Haskell


menor :: Int -> Int -> Int
menor a b = if a <= b then a else b

ispar :: Int -> Bool


ispar x = if mod x 2 == 0 then True else False

Outra forma de implementar as condicionais no Haskell é com o uso de “guardas”, que


são equações condicionais que especificam cada uma das circunstâncias nas quais
a definição da função pode ser aplicada, possibilitando o uso do otherwise, conforme
exemplos abaixo:

fatorial :: Int -> Int


fatorial n
| n == 0 = 1
| n > 0 = n * (fatorial (n-1))

menor :: Int -> Int -> Int


menor a b
| a <= b = a
| otherwise = b

15
Voltar ao sumário
É muito importante reforçar que o Haskell não utiliza chaves para delimitar partes do
código relacionadas. Redobre sua atenção com a indentação, pois linhas de código no
mesmo nível de indentação significam que elas pertencem a um mesmo bloco.

Em muitas situações, desejamos utilizar valores e funções auxiliares em uma definição


principal. O Haskell possibilita isso, através da cláusula Where. Essa cláusula traz
definições locais para uma equação, ou seja, o escopo das definições trazido por ela se
limita apenas à equação que a contém.

Por exemplo, vamos supor que queiramos calcular a área de um triângulo de lados a, b e
c, através da fórmula de Heron, dada por:

A = SQRT(s(s – a)(s – b)(s – c))

Onde s é o semiperímetro do triângulo:

s = (a + b + c) / 2

Conseguimos transpor essa formulação para Haskell:

area :: Float -> Float -> Float -> Float


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

//Exemplo de função que retorna a quantidade de raízes reais


utilizando guardas
equacao2g :: Float -> Float -> Float -> Int
equacao2g a b c
| delta > 0 = 2
| delta == 0 = 1
| otherwise = 0
where
delta = b^2 – 4*a*c

Note que podemos construir definições locais escrevendo uma expressão let, formada
por uma lista de definições e um corpo (que é uma expressão), conforme vemos abaixo:

let definições in expressão

Vejamos alguns exemplos:

16
Voltar ao sumário
Quadro 4 – Definição e expressão

Expressão Resultado

let x = 4 + 6 in x^2 + 2*x – 4 116

let x = 3 + 2; y = 5-1 in x^2 + 2*x – y 31

let quadrado x = x * x in quadrado 5 + quadrado 4 41

Fonte: elaborado pelo autor (2023)

Agora vamos juntar tudo, definindo uma função que calcula a área de um cilindro

areacilindro :: Float -> Float -> Float


areacilindro r h = let arealado = 2 * pi * r * h
areabase = pi * r * r
in arealado + 2 * areabase

É importante ressaltar que, no contexto das definições locais em programação, o uso


da palavra-chave “let” refere-se a uma expressão que pode ser empregada em qualquer
local onde se espera uma expressão. Por outro lado, ao utilizar a palavra-chave “where”,
as definições são posicionadas no final, visto que “where” não representa uma expressão
em si. Portanto, “where” só pode ser utilizado para realizar definições locais dentro de
uma definição de função.

Em Haskell, como não existe o conceito de estado do programa ou de variáveis de


controle, não existem estruturas de repetição, logo, toda repetição necessária será
efetuada através da recursão.

Uma função recursiva é uma função definida em termos dela mesma. Possui três fases:

1. A subdivisão do problema em subproblemas;


2. A condição de parada (base final) da recursão, quando sabemos a resposta do
problema para aquela entrada do domínio (caso base);
3. A combinação das respostas parciais, formando a resposta final.

Como exemplo de função recursiva, temos o fatorial apresentado anteriormente neste


Circuito. Abaixo segue sua definição.

fatorial :: Int -> Int


fatorial n
| n == 0 = 1
| n > 0 = n * (fatorial (n-1))

17
Voltar ao sumário
Na definição, o primeiro guarda estabelece que i fatorial de 0 é 1 (caso base). Já o segundo
guarda estabelece que o fatorial de um número positivo é o produto deste número e do
fatorial de seu antecessor. Esse é o caso recursivo e geral.

Recursão em cauda é um tipo especial de recursão, no qual o resultado da chamada


recursiva não precisa ser processado de maneira alguma para produzir o resultado final. A
linguagem Haskell otimiza chamadas com recursão em cauda, de maneira a economizar
recursos e aumentar a eficiência. No exemplo apresentado da função recursiva fatorial,
percebemos que não é um exemplo de recursão em cauda, pois para o resultado da
chamada de fatorial de n, existe a multiplicação de n por fatorial de (n-1) para produzir o
resultado.

Como exemplo de recursão em cauda, podemos apresentar a função que calcula


recursivamente a potência de 2 ajustada.

potenciade2emcauda :: Int -> Int -> Int


potenciade2emcauda n acumulado
| n == 0 = acumulado
| n > 0 = potenciade2emcauda (n-1) (2*acumulado)

Agora vamos exibir o passo a passo da execução desta chamada: potenciade2emcauda 5


1. Note que a chamada sempre diminui o valor de n enquanto que o resultado acumulado
sempre é enviado como segundo parâmetro da chamada

chamada
potenciade2emcauda 5 1
potenciade2emcauda 4 2
potenciade2emcauda 3 4
potenciade2emcauda 2 8
potenciade2emcauda 1 16
potenciade2emcauda 0 32

Por fim, apresentaremos a função fatorial ajustada para o requisito de função com
recursão em cauda.

fatrec :: Int -> Int -> Int


fatrec n acumulado
| n== 0 = acc
| n > 0 = fatrec (n-1) (n*acumulado)

Compreendidos os assuntos tratados até aqui? Então, vamos aprender mais, no nosso
próximo Circuito de Estudo!

18
3.

Voltar ao sumário
Módulos, funções de alta ordem, imutabilidade e efeitos
colaterais

Neste Circuito, vamos explorar cada um desses conceitos, apresentando sua importância
para a programação funcional.

Um módulo em programação funcional é uma estrutura que reúne um conjunto de


funções, tipos e typeclasses (em Haskell pode ser vista como uma espécie de interface
que define um conjunto de funções ou operações que os tipos pertencentes a ela devem
implementar). Ele permite organizar o código de forma modular e reutilizável, facilitando
o desenvolvimento de programas complexos.

Simon Thompson (2011) descreve um módulo como uma coleção de declarações de


nomes, definindo funções, tipos e classes. Por sua vez, Graham Hutton (2016) afirma que
módulos são usados para agrupar definições relacionadas em um único arquivo. Essas
definições ressaltam a capacidade dos módulos de agrupar elementos semelhantes e
facilitar a modularidade do código.

Ao dividir um programa em vários módulos, é possível obter diversas vantagens. Um


módulo suficientemente genérico pode fornecer funções úteis que podem ser aproveitadas
por diferentes softwares. Além disso, ao organizar o código em módulos independentes,
é possível reutilizá-los em outros projetos, promovendo a modularidade e a flexibilidade.
Essa abordagem requer um investimento de tempo para separar o código em partes com
propósitos adequados, mas as vantagens resultantes são significativas.

Na biblioteca padrão de Haskell, encontramos uma diversidade de módulos que


abrangem tipos e funções relacionadas. Esses módulos são projetados para facilitar
o desenvolvimento em áreas específicas, como manipulação de listas, programação
concorrente e operações com números complexos. Um exemplo é o módulo Prelude, que é
importado automaticamente e contém várias funções, tipos e typeclasses fundamentais.

1 import [Link]
2 numUniques :: (Eq a) => [a] -> Int
3 numUniques = length . nub

Observando o código apresentado acima, percebemos que para importar um módulo,


nesse script Haskell, utilizamos o comando import (linha 1). É importante lembrar que esta
declaração deve ser feita antes de qualquer definição de função, geralmente colocada
nas linhas iniciais do arquivo.

É possível importar múltiplos módulos, adicionando o código apropriado em linhas


separadas. Importamos o módulo [Link], que possui uma variedade de funções úteis
para trabalhar com listas, incluindo uma função que permite determinar a quantidade de

19
Voltar ao sumário
elementos únicos em uma lista específica. Ao utilizar a declaração «import [Link]»
em Haskell, passamos a acessar o módulo [Link] que fornece várias funções úteis
para manipulação de listas (linha 2 e 3). Abaixo estão algumas das principais funções
disponíveis nesse módulo:
Quadro 1 – Funções para manipulações de listas

Função Expressão

head Retorna o primeiro elemento de uma lista.

tail Retorna a cauda (todos os elementos, exceto o primeiro) de uma lista.

init Retorna todos os elementos de uma lista, exceto o último.

last Retorna o último elemento de uma lista.

null Verifica se uma lista está vazia.

length Retorna o tamanho (número de elementos) de uma lista.

reverse Inverte a ordem dos elementos em uma lista.

take Retorna os primeiros n elementos de uma lista.

drop Remove os primeiros n elementos de uma lista.

elem Verifica se um elemento está presente em uma lista.

maximum Retorna o elemento máximo de uma lista.

minimum Retorna o elemento mínimo de uma lista.

nub Remove todas as duplicações da lista.

sum Calcula a soma de todos os elementos numéricos em uma lista.

product Calcula o produto de todos os elementos numéricos em uma lista.

concat Concatena uma lista de listas em uma única lista.

intersperse Insere um elemento entre cada par de elementos em uma lista.

intercalate Concatena uma lista de listas, intercalando um separador entre elas.

transpose Transpõe as linhas e colunas de uma lista de listas.

subsequences Retorna todas as subsequências de uma lista.

permutations Retorna todas as permutações dos elementos de uma lista.

Fonte: elaborado pelo autor (2023)

As funções de alta ordem são funções que recebem uma outra função como argumento;
retornam funções como resultado; recebem e retornam uma função. Essas funções são
particularmente úteis para criar funções genéricas que dependem de certas funções
externas, que serão passadas como parâmetro. Inicialmente focaremos nos processos
de mapeamento e filtragem.

20
Voltar ao sumário
A seguir apresentaremos um exemplo de definição de função em Kaskell:

soma :: Int -> Int -> Int


soma

Vamos considerar o exemplo acima, no qual temos uma função chamada soma que
recebe dois números e retorna a soma deles. Normalmente, para utilizar essa função,
somos obrigados a fornecer os dois argumentos:

Chamada Resultado

soma 3 5 8

No entanto, para situações específicas, a programação funcional nos permite utilizarmos


o recurso da aplicação parcial que é uma técnica de fornecer menos argumentos do
que os necessários em uma função, resultando em uma nova função que espera os
argumentos restantes. Em outras palavras, uma aplicação parcial permite criar uma
nova função “especializada” a partir de uma função existente, fixando alguns de seus
argumentos, para a função soma, poderemos aplicar essa técnica assim:

Chamada Resultado

soma3 :: Int -> Int 5

soma3 = soma 3

resultado = soma3 2

Na linha soma3 = soma 3, estamos criando uma nova função chamada soma3 que é uma
aplicação parcial da função soma. Ela fixa o primeiro argumento como 3 e espera apenas
o segundo argumento. Portanto, quando chamamos soma3 2, estamos fornecendo o
argumento restante e obtendo o resultado esperado, que é 5 (3 + 2).

A aplicação parcial é útil porque nos permite criar funções mais especializadas a partir de
funções mais gerais. Isso pode simplificar o código, tornando-o mais legível e reutilizável.
Além disso, a aplicação parcial está intimamente relacionada ao conceito de currificação,
que é uma propriedade fundamental em Haskell e outras linguagens funcionais, onde
as funções sempre recebem um único argumento e retornam uma nova função como
resultado, se necessário.

Como parte da utilização do cálculo lambda, as funções podem ser definidas da seguinte
forma:

21
Voltar ao sumário
Quadro 2 – Definições de funções

Chamada Resultado

2:[] [2]

6:8:[] [6,8]

4:[6,8] [4,6,8]

Fonte: elaborado pelo autor (2023)

Recapitulando, funções de alta ordem são aquelas que operam sobre outras funções,
ou seja, que podem receber outra função com argumento ou retornar uma função como
resultado. Como exemplo, abordaremos duas funções muito utilizadas no processamento
de listas: mapeamento e filtros. Observe o código abaixo que implementa uma função que
recebe uma lista e retorna outra lista com os valores dos elementos multiplicados por 2.

dobra :: Int -> Int


dobra x = x + x

dobraLista :: [Int] -> [Int]


dobraLista [] = []
dobraLista (prim:outros) = (dobra prim) : (dobraLista outros)

Agora imagine que precisemos construir uma função que calcule o quadrado de um
número, a única troca para resolver a questão, seria substituir (x + x) por (x * x). É possível
implementar uma função de alta ordem que recebe uma função f do tipo (Int -> Int) como
argumento e a aplicar uniformemente em todos os elementos da lista, retornando uma
lista de inteiros mapeada.

quadrado :: Int -> Int


quadrado x = x * x

quadradoLista :: [Int] -> [Int]


quadradoLista [] = []
quadradoLista (prim:outros) = (quadrado prim) : (quadradoLista
outros)

O que nós buscamos, com a implementação de uma função de alta ordem, é a criação de
uma única função que trabalhe com as duas situações apresentadas, tanto com a função
dobra, quanto com a função quadrado.

22
Voltar ao sumário
dobra :: Int -> Int
dobra x = x + x

quadrado :: Int -> Int


quadrado x = x * x

mapInt :: (Int -> Int) -> [Int] -> [Int]


mapInt _ [] = []
mapInt f (prim:outros) = (f prim) : (mapInt outros)

//Chamada
mapInt dobra [10, 20, 30, 40]
//resultado
[20, 40, 60, 80]

//Chamada
mapInt quadrado [1, 2, 3, 4]
//resultado
[1, 4, 9, 16]

Como próximo exemplo, vamos criar uma função de alta ordem chamada “Moema”, que
recebe como argumento uma função de teste e seleciona os elementos da lista que
satisfazem a condição desejada.

moema :: (Int -> Bool) -> [Int] -> [Int]


moema f [] = []
moema f (cab:cauda)
| (f cab) == True = cab : (filtro f cauda)
| otherwise = filtro f cauda

pares :: Int -> Bool


pares x = (mod x 2 == 0)

impares :: Int -> Bool


impares x = (mod x 2 == 1)

23
Voltar ao sumário
//chamada
lista = [1 .. 20]
moema pares lista
//resultado
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

O Haskell possui funções próprias (built-in) para realizar o mapeamento (map) e a


filtragem (filter) de elementos.

Chamada Resultado

lista = [1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 4, 9, 16, 25, 36,


49, 64, 81]
let dobro x = x * x in map dobro lista

lista = [1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 3, 5, 7, 9]

let impares x = (mod x 2 == 1) in filter


impares lista

lista = [1, 2, 3, 4, 5, 6, 7, 8, 9] [6, 7, 8, 9]

filter (>5) lista

lista = [1, 2, 3, 4, 5, 6, 7, 8, 9] [11, 12, 13, 14, 15,


16, 17, 18, 19]
map (+10) lista

A documentação oficial do Haskell define módulos como unidades de organização e


encapsulamento de código em um programa Haskell. Os módulos fornecem uma maneira
de agrupar funções, tipos de dados e typeclasses relacionados em uma unidade lógica,
permitindo a reutilização e a modularização do código.

Um módulo em Haskell é declarado usando a palavra-chave module, seguida pelo nome


do módulo. O código associado ao módulo é delimitado por chaves {} e pode conter
definições de funções, tipos, typeclasses, instâncias e outras declarações.

Os módulos em Haskell também podem exportar uma lista específica de funções, tipos
ou typeclasses para serem visíveis para outros módulos. Isso é feito usando a palavra-
chave export, seguida pela lista de elementos a serem exportados.

Os módulos podem ser importados em outros módulos usando a palavra-chave import,


seguida pelo nome do módulo a ser importado. Isso permite o uso das definições
exportadas pelo módulo importado no módulo atual.

Através do uso de módulos, é possível criar uma estrutura organizada para o código
Haskell, dividindo-o em partes menores e mais gerenciáveis. Isso facilita a manutenção,
reutilização e colaboração em projetos de programação funcional.

24
Voltar ao sumário
No exemplo apresentado, para o módulo [Link], que contém uma variedade de
funções úteis para manipulação de listas, essas funções são exemplos de mapeamento
e filtragem. O mapeamento consiste em aplicar uma função a todos os elementos de
uma lista, enquanto a filtragem envolve selecionar os elementos que satisfazem uma
determinada condição.

Além disso, no contexto das funções de alta ordem, foram apresentados exemplos de
aplicação parcial e currificação. A aplicação parcial consiste em fixar determinados
argumentos de uma função para obter uma nova função especializada, e a currificação é
o conceito de que as funções recebem um único argumento e retornam uma nova função,
se necessário.

É relevante mencionar que a combinação desses conceitos de módulos, funções de


alta ordem, mapeamento e filtragem proporciona uma abordagem modular e flexível
na programação funcional, onde é possível criar código legível, reutilizável e de fácil
manutenção. O Haskell, por sua vez, oferece funções incorporadas (built-in) como o
«map» e o «filter» para realizar o mapeamento e a filtragem de elementos, simplificando
a implementação e melhorando a legibilidade do código.

Chegou o momento de aprofundar mais os conhecimentos estudados neste Circuito. Leia


o material de estudo e depois teste os seus conhecimentos respondendo as atividades
de fixação!

25
4.

Voltar ao sumário
Organização e manipulação de dados imutáveis: listas,
tuplas e maps

Dentro da programação funcional e todas as suas característica, é importante perceber


que a estrutura de dados fundamental deste paradigma é a lista, uma coleção de
elementos de um certo tipo que pode ser definida pela enumeração de seus elementos
(ASCENCIO 2012).

Para declarar uma lista, é necessário informar o tipo da lista entre colchetes, como uma
lista de inteiros [Int} ou de caracteres [Char] ou de valores lógicos [Bool].

Nessas listas, os elementos são definidos através da inserção dos valores entre os
colchetes, separando cada valor do outro através de uma virgula, como podemos observar
abaixo:

listanum = [1, 2, 3, 4, 5]
listavazia = []
listachar = [‘a’,’b’,’c’,’d’,’e’]

Vale ressaltar que além da inicialização simples, as listas também podem ser definidas
através da criação de lista de listas:

listas = [[1,2], [3,4], [5,6]]


listacasais = [[“Leo,”Roberta”],[“Paulo”,”Alda”],[“Edgard”,
“Silvia”],[“Priscila”,”Érico”]]

Ou a lista pode ser preenchida automaticamente, através do reconhecimento de um


padrão, como em:

1 ls1 = [1 .. 10]
2 ls2 = [1,3 .. 10]
3 ls3 = [10, 8 .. 0]
4 ls4 = [1,1,2,3,5,8..50]

Na linha 01 foi criada uma lista contendo os elementos numerados em sequência: [1,
2, 3, 4, 5, 6, 7, 8, 9, 10]. Já na linha 2 foi criada uma lista com os elementos ímpares
começando em 1 indo até 10: [1, 3, 5, 7, 9]. Na linha 03 foram criados os números pares
em ordem decrescente indo de 10 a 0: [10, 8, 6, 4, 2, 0]. Por fim, na linha 04, temos uma
lista criada para representar os primeiros elementos da série de Fibonacci, composta
pelos elementos: [1, 1, 2, 3, 5, 8, 13, 21, 34].

26
Voltar ao sumário
O Haskell oferece dois operadores básicos para manipulação de listas. O operador
interfixado “:”, que é um elemento e uma lista (que pode estar vazia) como seus
argumentos, possibilita a construção de uma lista, elemento por elemento. Importante
lembrar que o parâmetro da esquerda sempre será um elemento e o parâmetro da
esquerda sempre será uma lista, conforme podemos observar abaixo:
Quadro 1 – Elemento e parâmetro

Chamada Resultado

2:[] [2]

6:8:[] [6,8]

4:[6,8] [4,6,8]

Fonte: elaborado pelo autor (2023)

Em Haskell, as funções head e tail são usadas para manipular listas.

A função head recebe uma lista como argumento e retorna o primeiro elemento dessa
lista. Por exemplo, se tivermos uma lista ls1 = [1, 2, 3, 4], a chamada da função head
ls1 retornará o valor 1. No entanto, é importante notar que a função head só pode ser
chamada em listas não vazias. Se tentarmos chamar head em uma lista vazia, ocorrerá
um erro.

A função tail, por outro lado, recebe uma lista como argumento e retorna uma nova lista
contendo todos os elementos, exceto o primeiro. Continuando com o exemplo anterior,
se chamarmos tail ls1, obteremos a lista [2, 3, 4]. Novamente, assim como head, a função
tail só pode ser chamada em listas não vazias, pois não é possível remover o primeiro
elemento de uma lista vazia.

Baseados nos comandos “:” tail e head, podemos entender que a lista [1, 2, 3, 4] pode
ser escrita como 1 : [2, 3, 4], onde 1 :: Int é a cabeça da lista (head) e a lista [2, 3, 4] :: Int
é a sua cauda (tail). Logo uma lista ou é vazia ou é uma lista com cabeça seguida de
cauda. Abaixo implementamos uma função recursiva que calcula o comprimento da lista
de inteiros:

comp :: [Int] -> Int


comp [] = 0
comp (cabeca : cauda) = 1 + comp cauda

Outro exemplo é o programa abaixo que recebe uma lista e devolve outra lista, que contém
os valores da primeira lista elevados à quinta potência

27
Voltar ao sumário
penta:: Int -> Int
penta x = x * x * x * x * x

aQuinta :: [Int] -> [Int]


aQuinta [] = []
aQuinta (cabeca: cauda) = penta(cabeca) : aQuinta cauda

Nota-se que a função aQuinta não possui recursão em cauda. Na chamada recursiva,
realizamos o processamento da cabeça (elevando o número à quinta potência) para
posteriormente conectá-lo à cauda da lista que ainda vai ser processada recursivamente.

Assim, pela aplicação da função acima, obteremos o seguinte resultado para a entrada
testada:

lista = [1 .. 9]
aQuinta lista
//Resultado
[1, 32, 243, 1024, 3125, 7776, 16807, 32768, 59049]

O Haskell também oferece um operador útil para realizar a concatenação de duas listas.

pares = [0, 2 .. 8]
impares = [1, 3 .. 9]
pares ++ impares //gerará a lista resultante [0, 2, 4, 6, 8, 1,
3, 5, 7, 9]
[1, 2, 3] ++ [4, 5, 6] //gerará a lista resultante [1, 2, 3, 4,
5, 6]

Uma lista também pode ser definida através de um critério Gerador, como exemplo: crie
uma lista de todos os pares (2*x) tal que x é uma lista de números entre 0 e 10.

numerosPares = [2*x | x <- [0..10]]


//Resultado
[0, 2, 4, 6, 8, 10, 12, 14, 16 , 18, 20]

Uma extensão dessa lista poderia ser definida para gerar uma lista infinita de números
pares:

todosPares = [2*x | x <- [0, 1 .. ]]


//Resultado
[0, 2, 4, 6, 8, 10, 12, 14, 16 , 18, 20, 22, 24, 26, 28, 30,
.....]

28
Voltar ao sumário
Isso é possível devido à implementação do mecanismo de avaliação lenta, que se refere
à estratégia de avaliação adotada pela linguagem Haskell, em que as expressões não
são avaliadas imediatamente, mas apenas quando seu valor é necessário. Isso significa
que a linguagem Haskell usa a avaliação lenta como a estratégia de avaliação padrão. A
avaliação lenta é possível devido ao mecanismo de lazy instantiation, que é uma técnica
usada por Haskell para implementar a avaliação lenta.

A instanciação tardia (lazy instantiation) é um mecanismo específico da linguagem


Haskell que permite que as expressões sejam computadas apenas quando necessário.
Isso é feito usando thunks e atrasando a avaliação até que o valor seja solicitado. A
instanciação tardia é uma característica fundamental de Haskell que permite a criação
de estruturas de dados infinitas e torna a avaliação mais flexível.

Para finalizarmos o assunto listas, vamos demonstrar a implementação do algoritmo de


ordenação de vetores/listas chamado “QuickSort” que possui basicamente 3 passos:

A
Escolhe um elemento do vetor
para ser o pivô.

B
Particiona o vetor de maneira que
todos os elementos anteriores ao pivô
sejam menores do que ele, e todos os
elementos posteriores sejam maiores.

C
Ordena recursivamente os vetores
de elementos menores e maiores.

A base da recursão são os vetores de tamanho 0 ou 1, que já se encontram ordenados. A


cada passo do algoritmo, pelo menos um elemento é colocado em sua posição definitiva,
e não será mais manipulado no passo seguinte.

qsort :: [Int] -> [Int]


qsort [] = []
qsort (cab : cauda) = qsort [y | y <- cauda, y < cab] //menores
que o pivô
++ [cab] //o próprio pivô
++ qsort [y | y <- cauda, y >= cab] //maiores que o pivô

29
Voltar ao sumário
Tuplas

Uma Tupla é uma coleção de valores que pode ou não ter tipos diferentes, os valores são
colocados entre parênteses e separados por vírgulas, como no exemplo: (“Unifor”,1)

Os valores de tupla são definidos de maneira semelhante às listas, mas com a utilização
de parênteses () em vez de colchetes [].

Além disso, uma tupla pode conter calores de tipos diferentes, enquanto uma lista não.
Note que dentro da tupla a ordem dos elementos importa, de maneira que a tupla (1,
“Unifor”) é diferente da tupla (“Unifor”,1).

Quando a tupla possui dois membros, ela é chamada de “par” ou 2-pla. Para esse caso
especial de tupla, foram desenvolvidas duas funções, uma para acessar o primeiro
elemento da tupla (fst) e uma para acessar o segundo elemento da tupla (snd):

Função Sintaxe Resultado

Fst fst (“Unifor” ,1) “Unifor”

Snd snd (“Unifor” ,1) 1

Para desenvolver um sistema de classificação de times em um campeonato, por exemplo,


é necessário manipular informações sobre times. Podemos representar um time através
de uma tupla em Haskell:

type NomeTime = String


type Pontos = Int
type Time = (NomeTime, Pontos)
type Campeonato = [Time]

O uso do comando type permite que o desenvolvedor crie um sinônimo para um tipo. Sua
utilização não cria um novo tipo, apenas dá um apelido a tipos já existentes, tornando a
legibilidade do programa melhor.

Com base na definição do tipo Time, podemos elaborar funções para acessar os elementos
da tupla, utilizando casamento de padrões. Para exemplificar, vamos criar uma função
para apresentar os times classificados para a fase final da competição.

classificados :: Campeonato -> Int -> [NomeTime]


classificados cpto pontos = [nome | (nome, ptos) <- cpto, ptos >=
pontos]

Assim, para um campeonato composto pela lista abaixo:

30
Voltar ao sumário
brasileirão = [(“Botafogo”,18), (“Palmeiras”,15),
(“Fluminense”,13), (“Atletico MG”,13),( “Cruzeiro”,11)]
classificados brasileirão 15

//Resultado
[“Botafogo”, ”Palmeiras”]

Para melhor fixar o uso de tuplas, vamos agora implementar um programa que crie a
representação de um ponto tridimensional (x,y,z), em que a sua representação será por
meio de uma tupla com a definição de um novo tipo e depois escreveremos uma função
que calcule a distância entre dois pontos passados como argumentos.

type Ponto = (Float, Float, Float)


distancia :: Ponto -> Ponto -> Float
distancia (x1, y1, z1) (x2, y2, z2) = sqrt(dx^2 + dy^2 + dz
^2)
where
dx = x1-x2
dy = y1-y2
dz = z1-z2

//Teste
p1 = (0, 0, 1)
p2 = (0, 0, 0)
distancia p1 p2

//Resultado
1.0

O casamento de padrões (pattern matching) é uma poderosa técnica usada em Haskell


para combinar padrões em expressões e realizar diferentes ações com base nos padrões
correspondentes. É uma forma de decompor estruturas de dados complexas em partes
menores e executar comportamentos específicos com base nesses padrões.

O casamento de padrões é comumente usado em várias construções da linguagem


Haskell, como declarações de função, expressões case e definição de tipos de dados.
Dado o exemplo:

31
Voltar ao sumário
padroes :: Int -> String
padroes 1 = “Um!”
padroes 2 = “Dois!”
padroes 3 = “Tres!”
padroes x = “O numero“ ++ show x ++ “não está entre 1 e 3!”

Ao chamar a função, passando um parâmetro, a linguagem vai tentar definir em qual


padrão esse parâmetro se encaixa, testando de cima para baixo os padrões fornecidos
pelo programador. O primeiro a ser encontrado é executado com o valor passado como
parâmetro.

Note que o exemplo apresentado acima, nomeamos os demais inteiros com x para utilizá-
lo na mensagem dos casos que não foram tratados. Caso não precisemos manipular o
valor de argumento, poderíamos substituir o x por um _ indicando ao Haskell que não
iremos utilizar essa informação para nada dentro do casamento de padrões.

O chamado padrão curinga, representado pelo caractere “_” pode ser utilizado no Haskell,
tendo em mente o casamento de padrões próprio da linguagem, para representar dados
indefinidos (variáveis) ou dados que não sejam relevantes para o seu programa (não
precisam ser nomeados). A utilização do padrão curinga facilita a implementação de
funções que utilizam o casamento de padrões para chegar ao seu resultado, muitas
vezes, funcionando como uma alternativa para estrutura de controle if-then-else muito
grandes, conforme podemos observar abaixo:

f1 :: Int -> Int -> Int -> Int f2 :: Int -> Int -> Int -> Int
f1 xyz | (x == 1) = 10 f2 1_ _= 10
| (y == 2) = 20 f2 _ 2 _ = 20
| (y == 3) = 30 f2 _ _ 3 = 30
| otherwhise = 0 f2 _ _ _ = 0

Para aprofundar esse tema, que tal ver agora o material complementar?

32
Voltar ao sumário
REFERÊNCIAS

ASCENCIO, Ana Fernanda Gomes; CAMPOS, Edilene Aparecida Veneruchi de. Fundamentos
da programação de computadores: algoritmos, pascal, c/c++ e java. 3. ed. São Paulo:
Pearson Education do Brasil, 2012. (Cód.:83968)

GHEZZI, Carlo. Conceitos de linguagens de programação. Colaboração de Mehdi Jazayeri.


Tradução Paulo A. S Veloso. 2. ed. Rio de Janeiro: Campus, 1987. (Cód.:5506)

SEBESTA, Robert W. Conceitos de linguagens de programação. 11. ed. Porto


Alegre: Bookman, 2018. Disponível em: [Link]
books/9788582604694. (DIGITAL) (Cód.:23617)

Hudak, P. (1999). The Haskell School of Expression: Learning Functional Programming


through Multimedia. Cambridge University Press.

Thompson, S. (2011). Haskell: The Craft of Functional Programming. Addison-Wesley


Professional.

33
UNIVERSIDADE DE FORTALEZA (UNIFOR) AUTOR
LEANDRO DA SILVA TADDEO
Presidência
Lenise Queiroz Rocha
Bacharel em Informática, Mestre em Informática Aplicada,
Vice-Presidência
26 anos de experiência em docência, 30 anos de experiência
Manoela Queiroz Bacelar
em TI. Pesquisador, Professor de graduação em Sistemas
Reitoria de Informação e professor de Pós-graduação em Design
Randal Martins Pompeu Digital. Atualmente, atua como analista judiciário, lotado
Vice-Reitoria de Ensino de Graduação e Pós-Graduação no setor de informática do Fórum Clovis Beviláqua.
Maria Clara Cavalcante Bugarim

Vice-Reitoria de Pesquisa
José Milton de Sousa Filho

Vice-Reitoria de Extensão
Thiago Braga Martins

Vice-Reitoria de Administração
José Maria Gondim Felismino Júnior

Diretoria de Comunicação e Marketing


Ana Leopoldina M Quezado Vargas Valle

Diretoria de Planejamento
Marcelo Nogueira Magalhães

Diretoria de Tecnologia
José Eurico de Vasconcelos Filho

Diretoria do Centro de Ciências da Comunicação e Gestão


Danielle Batista Coimbra

Diretoria do Centro de Ciências da Saúde


Lia Maria Brasil de Souza Barroso

Diretoria do Centro de Ciências Jurídicas


Katherinne de Macêdo Maciel Mihaliuc

Diretoria do Centro de Ciências Tecnológicas


Jackson Sávio de Vasconcelos Silva

RESPONSABILIDADE TÉCNICA

COORDENAÇÃO GERAL DE EDUCAÇÃO A DISTÂNCIA

Coordenação Geral Identidade Visual / Arte


Andrea Chagas Alves de Almeida Thiago Bruno Costa de Oliveira
Supervisão de Equipe Multidisciplinar Editoração / Diagramação
Francisco Weslley Lima Rebeka Melo Peres
Analista de Mídias Produção de Áudio e Vídeo
Emanoel Alves Cavalcante José Moreira de Sousa
Design Instrucional Programação / Implementação
Jéssica Gabrielle de Menezes Lima Renan Alves Diniz
Thais Rozas Teixeira
Revisão Gramatical
José Ferreira Silva Bastos

Você também pode gostar