Lambda, currying e ordem superior

Aplicação parcial, secções de operadores e pipelines com map, filter e foldr.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

soma 5 pode ser usada como uma função que acrescenta cinco ao próximo argumento. Isto acontece porque soma :: Int -> Int -> Int recebe um inteiro e devolve uma função Int -> Int. Esta forma de representar vários argumentos chama-se currying, e fixar alguns deles chama-se aplicação parcial.

Uma função de ordem superior recebe ou devolve uma função. map, por exemplo, recebe a transformação a aplicar a cada elemento; filter recebe o teste a aplicar. É esta parametrização por operações, e não apenas por dados, que torna estas funções reutilizáveis.

Aplicação parcial

soma :: Int -> Int -> Int
soma x y = x + y

soma5 :: Int -> Int
soma5 = soma 5

soma 5 é uma função à espera do segundo argumento, por isso soma5 3 dá 8. O mesmo vale para operadores por meio de secções: (>= 9.5) é a função que testa se o seu argumento é maior ou igual a 9.5, e (* 2) é a função que dobra. Quando precisares de uma função pequena só uma vez, escreve-a anónima com lambda: (\x -> x * 2) lê-se “a função que a x associa x * 2”.

As lambdas, também usadas em FP, definem funções sem nome. A aplicação parcial permite especializar uma função já existente sem escrever outra lambda. Atenção à subtração: (-2) é o número negativo; para retirar dois ao argumento, usa \x -> x - 2 ou subtract 2.

map, filter e foldr

O Prelude, importado por omissão, inclui estas funções de ordem superior. Estes tipos são as versões especializadas a listas:

map    :: (a -> b) -> [a] -> [b]
filter :: (a -> Bool) -> [a] -> [a]
foldr  :: (a -> b -> b) -> b -> [a] -> b

map transforma cada elemento, filter conserva os que passam no teste e foldr substitui a estrutura da lista por aplicações de uma operação, associadas à direita. A associação não obriga a avaliar primeiro a cauda. sum é foldr (+) 0 e length é foldr (\_ n -> n + 1) 0. Quando o problema for “transformar cada um”, “ficar só com alguns” ou “resumir tudo”, começa por estas antes de escreveres recursão à mão.

Média das aprovações

Dada uma pauta, queremos calcular a média das notas de aprovação. As notas podem ter parte decimal, por isso usamos [Double]. O resultado é Maybe Double, porque uma pauta sem aprovações não tem essa média; Maybe a tem dois construtores, Nothing para ausência e Just x para presença, e a expressão case escolhe a equação pelo construtor, como os padrões das funções.

mediaAprovados :: [Double] -> Maybe Double
mediaAprovados notas = case filter (>= 9.5) notas of
  [] -> Nothing
  xs -> Just (sum xs / fromIntegral (length xs))

Em [8,12,6,15,10], filter conserva [12,15,10]. A soma é 37 e o comprimento é 3, dando Just 12.333333333333334, porque a soma já é Double e só precisamos de converter o comprimento, que é Int. Com [2,7], o filtro produz [] e o resultado é Nothing. Podes usar where para dar nome a etapas intermédias, enquanto esta versão usa case para tratar as duas formas do resultado do filtro.

Média segura, folds e prefixos
mediaAprovados :: [Double] -> Maybe Double
mediaAprovados notas = case filter (>= 9.5) notas of
  [] -> Nothing
  xs -> Just (sum xs / fromIntegral (length xs))

main :: IO ()
main = do
  print (mediaAprovados [8,12,6,15,10])
  print (mediaAprovados [2,7])
  print (foldr (-) 0 [1,2,3::Int])
  print (foldl (-) 0 [1,2,3::Int])
  print (take 5 (map (^2) [1..] :: [Integer]))
Resultado

Executa o código para ver a saída e os gráficos.

Uma versão que calcula sum xs / fromIntegral (length xs) na lista vazia faz 0/0, que em Double dá NaN, ou seja não é uma média. Além disso, a assinatura [Int] é incompatível com o literal 9.5 no filtro, por isso um tipo correto e um caso de ausência explícito resolvem problemas diferentes.

Do caso recursivo ao foldr

Escreve primeiro a soma por recursão:

soma [] = 0
soma (x:xs) = x + soma xs

Substitui [] por z e (:) por f. Obténs foldr f z: o caso vazio devolve z e o caso recursivo aplica f à cabeça e ao resultado da cauda. Assim sum = foldr (+) 0 e length = foldr (\_ n -> n + 1) 0. Escolhe foldr quando cada passo só precisa da cabeça e do resumo da cauda. Escolhe filter para selecionar e takeWhile para parar no primeiro que falha.

foldr contra foldl

O foldr combina da direita e o foldl da esquerda, na forma como as aplicações ficam aninhadas. Ambos percorrem a lista da esquerda para a direita; “direita” e “esquerda” descrevem os parênteses, não o ponto de partida. A diferença aparece quando a operação não é associativa. Compara a subtração:

somaD :: [Int] -> Int
somaD = foldr (+) 0

somaE :: [Int] -> Int
somaE = foldl (+) 0

foldr (-) 0 [1,2,3] calcula 1 - (2 - (3 - 0)), que dá 2. Já foldl (-) 0 [1,2,3] calcula ((0 - 1) - 2) - 3, que dá -6. Com inteiros sem overflow e (+), os dois concordam, e é por isso que o exemplo da média não distingue; com (-), a direção muda o resultado. (:) cabe diretamente em foldr; em foldl, é preciso inverter a ordem dos argumentos, como em flip (:).

soma :: [Int] -> Int
soma = foldr (+) 0
-- soma [1,2,3] = 1 + (2 + (3 + 0)) = 6
soma :: [Int] -> Int
soma [] = 0
soma (x:xs) = x + soma xs
-- soma [1,2,3] = 1 + (2 + (3 + 0)) = 6

Listas infinitas e preguiça

Haskell só calcula o que é preciso para responder, por isso uma lista infinita é um valor legítimo enquanto só consumires um prefixo finito:

ghci> take 5 (iterate (+1) 0)
[0,1,2,3,4]
ghci> take 3 (repeat "ola")
["ola","ola","ola"]

iterate (+1) 0 começa em 0 e obtém cada elemento aplicando (+1) ao anterior. take 5 exige apenas cinco células; a continuação fica por avaliar. length precisa de encontrar o fim, por isso não termina nesta lista infinita.

Escolher a função de ordem superior

takeWhile p conserva apenas o prefixo que satisfaz p; filter p continua a procurar depois de um elemento falhar. Produzir todo o resultado de uma lista finita exige percorrê-la; pedir só um prefixo pode exigir menos. Em [2,4,1,6], takeWhile even dá [2,4], enquanto filter even dá [2,4,6]. dropWhile even dá [1,6]. O 6 já não é testado para terminar o prefixo: a fronteira foi o primeiro 1.

all p xs é verdadeiro se todos os elementos satisfazem p; any p xs basta-se com um. Na lista vazia, all dá True e any dá False. Estas identidades permitem escrever and = foldr (&&) True e or = foldr (||) False.

Uma composição aplica primeiro a função mais à direita. Para sum . map (^2) . filter even em [1,2,3,4], seguimos:

EtapaResultado
filter even[2,4]
map (^2)[4,16]
sum20

Se precisas de ver a entrada de cada etapa, escreve primeiro a versão com argumento e nomes locais; só depois retira o argumento.

Se a dúvida é como a composição, o map e o fold se combinam, vê este vídeo e prevê cada resultado:

Abrir no YouTube

Derivar um fold

Para substituir a recursão de append xs ys, queremos trocar cada (:) de xs por (:) e o [] final por ys. Logo append xs ys = foldr (:) ys xs. Para concat, cada elemento já é uma lista: concat = foldr (++) [].

Inverter com foldl (flip (:)) [] coloca cada novo elemento na cabeça do acumulador. Os estados para [1,2,3] são [], [1], [2,1], [3,2,1]. Inverter com foldr (\x acc -> acc ++ [x]) [] também funciona numa lista finita, mas as concatenações repetidas tornam o tempo quadrático. A primeira versão percorre a lista em tempo linear.

O que a preguiça permite

Haskell só exige um resultado intermédio quando precisa dele. take 3 [1..] pede três células e termina; sum [1..] exige o fim de uma lista que não o tem. False && undefined é False, mas True && undefined falha ao pedir o segundo argumento. A ordem dos argumentos importa para este curto-circuito.

foldr (&&) True (False:repeat True) termina com False, porque && pode ignorar a cauda. foldl (&&) True nessa lista precisa de chegar ao fim e não termina. foldr não é uma promessa geral de terminação em listas infinitas: foldr (+) 0 [1..] também exige a cauda inteira.

foldl pode acumular uma cadeia de expressões por avaliar. foldl' de Data.List força o acumulador a cada passo até à forma normal fraca. É útil para somas grandes, mas não força necessariamente o interior de uma árvore ou de um tuplo. Distingue associatividade da operação, ordem de avaliação e custo em memória.

Contar um intervalo numa lista infinita ordenada

Queremos os primos entre 10 e 30, incluindo as fronteiras. A lista de primos está em ordem crescente. Primeiro retiramos os que são menores que 10; depois conservamos só o prefixo até 30:

intervalo inicio fim = takeWhile (<= fim) . dropWhile (< inicio)

O resultado é [11,13,17,19,23,29], com seis elementos. dropWhile chega ao primeiro primo admissível, 11. takeWhile para ao observar 31; pela ordenação, nenhum dos primos posteriores pode voltar ao intervalo. Assim, length só percorre a lista finita que acabou de ser delimitada.

Delimitar antes de contar os primos
primo :: Integer -> Bool
primo n = n > 1 && testar 2
  where
    testar d
      | d*d > n = True
      | n `mod` d == 0 = False
      | otherwise = testar (d+1)

primos :: [Integer]
primos = filter primo [2..]

intervalo :: Ord a => a -> a -> [a] -> [a]
intervalo inicio fim = takeWhile (<= fim) . dropWhile (< inicio)

main :: IO ()
main = do
  print (intervalo 10 30 primos)
  print (length (intervalo 10 30 primos))
  print (intervalo 13 23 primos)
Resultado

Executa o código para ver a saída e os gráficos.

length (filter (\p -> 10 <= p && p <= 30) primos) não termina: depois de 29, o filtro continua a procurar para sempre outro elemento que passe. Mesmo produzindo os seis elementos iniciais corretos, não consegue devolver o fim da lista. take 30 também responderia a outra pergunta: conta posições, não valores.

A ordenação é uma hipótese essencial. Numa lista arbitrária, parar no primeiro valor acima de 30 poderia perder um valor menor que aparecesse depois. Numa entrada infinita, precisamos também de alcançar a fronteira de paragem em passos finitos; uma lista infinita de 20 não a tem. Para listas finitas ordenadas, o fim da própria lista também permite terminar.

Exercícios

Associar pela direita

Quanto vale foldr (-) 0 [2,4,6]?

Pista

Expande antes de calcular.

Solução

6-0=6; 4-6=-2; 2-(-2)=4.

Erros frequentes

Não comeces pelo acumulador à esquerda, que corresponderia a foldl.

Voltar à explicação
Prefixo ou seleção completa?

Qual é takeWhile even [4,2,3,8]?

A tua resposta

Pista

takeWhile termina no primeiro elemento que falha.

Solução

O resultado é [4,2]. O último 8 não pertence ao prefixo inicial de pares.

  • [4,2]. Para ao encontrar 3.
  • [4,2,8]. Este é o resultado de filter even.
  • [3,8]. Este é o resultado de dropWhile even.

Erros frequentes

Não transformes uma função de prefixo num filtro.

Voltar à explicação
Um fold pode terminar?

Explica por que foldr (||) False ( True:repeat False) termina e foldl (||) False nessa lista não termina.

Pista

True || qualquerCoisa não precisa de avaliar qualquerCoisa.

Solução

foldr produz True || foldr … e o primeiro True chega. foldl passa um novo acumulador mas continua até ao caso []; esse caso nunca é atingido.

Confere a tua resposta:

  • Uso o curto-circuito de ||.
  • Distingo pedir a cauda de devolver True.
  • Identifico que foldl precisa do fim da lista.

Erros frequentes

A associatividade booleana não torna iguais os comportamentos de terminação.

Voltar à explicação
Derivar concatenação e inversão com folds

Define myappend xs ys com foldr e myreverse xs com foldl, sem recursão explícita. Explica as operações e os valores iniciais escolhidos. Segue myreverse [1,2,3] e compara o custo com foldr (\x acc -> acc ++ [x]) [] em listas finitas.

Pista
Identifica o fim que append deve substituir e a ponta onde reverse deve colocar cada elemento.
Solução

Em myappend xs ys = foldr (:) ys xs, cada célula de xs é reconstruída com (:) e o fim é substituído por ys. Por exemplo, [1,2] e [3] dão 1:(2:[3]).

Em myreverse xs = foldl (flip (:)) [] xs, flip (:) acc x vale x:acc. Os acumuladores são [], [1], [2,1] e [3,2,1]. Cada passo acrescenta uma célula, dando tempo linear.

O fold pela direita também inverte, mas acc ++ [x] percorre um acumulador crescente. O trabalho total é 0+1+...+(n-1), de ordem quadrática.

Confere a tua resposta:

  • Derivo append com foldr e caso base ys.
  • Derivo reverse com foldl e flip (:).
  • Mostro os estados do acumulador e comparo custos.

Erros frequentes

A ordem dos argumentos de (:) é elemento primeiro e lista depois.
Voltar à explicação
Delimitar valores antes de contar

primos é a lista infinita crescente de primos da lição. Escreve uma expressão que conte os primos entre 13 e 23, incluindo ambos. Explica por que length (filter (\p -> 13 <= p && p <= 23) primos) não termina e por que take 23 não fixa o limite superior pretendido. O mesmo método de prefixos funcionaria numa lista infinita que repetisse 20?

Solução

length (takeWhile (<=23) (dropWhile (<13) primos)) conta [13,17,19,23], dando 4. A condição inferior retira só valores menores que 13; a superior inclui 23 e para ao observar 29. Pela ordenação, não há nenhum candidato posterior dentro do intervalo.

O filtro produz esses quatro valores, mas continua a procurar para sempre e não devolve o fim da lista; length precisa desse fim. take 23 escolheria os primeiros 23 elementos da lista que recebesse, independentemente dos seus valores.

Uma repetição infinita de 20 satisfaz sempre o limite superior. takeWhile nunca encontra a fronteira e contar não termina. Ordem por si só não garante que se atinja um valor acima do limite em passos finitos.

Confere a tua resposta:

  • Uso dropWhile (<13) e takeWhile (<=23) antes de length.
  • Obtenho quatro primos com as duas fronteiras incluídas.
  • Justifico a paragem pela ordem e pelo primeiro valor acima do limite.
Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.