Começando

Preparar, apontar, já!

egg Tudo bem, vamos começar! Se você é o tipo de pessoa horrível que não lê introduções às coisas e a pulou, talvez queira ler a última seção da introdução, porque ela explica o que você precisa para seguir este tutorial e como vamos carregar as funções. A primeira coisa que faremos é executar o modo interativo do GHC e chamar alguma função para ter uma noção muito básica de Haskell. Abra seu terminal e digite ghci. Você será recebido com algo assim.

GHCi, version 9.2.4: https://www.haskell.org/ghc/  :? for help
ghci>

Parabéns, você está no GHCI!

Aqui está um pouco de aritmética simples.

ghci> 2 + 15
17
ghci> 49 * 100
4900
ghci> 1892 - 1472
420
ghci> 5 / 2
2.5
ghci>

Isso é bastante autoexplicativo. Também podemos usar vários operadores em uma linha e todas as regras de precedência usuais são obedecidas. Podemos usar parênteses para tornar a precedência explícita ou para alterá-la.

ghci> (50 * 100) - 4999
1
ghci> 50 * 100 - 4999
1
ghci> 50 * (100 - 4999)
-244950

Muito legal, né? Sim, eu sei que não é, mas tenha paciência comigo. Uma pequena armadilha a ser observada aqui é negar números. Se queremos ter um número negativo, é sempre melhor cercá-lo com parênteses. Fazer 5 * -3 fará o GHCI gritar com você, mas fazer 5 * (-3) funcionará muito bem.

A álgebra booleana também é bastante direta. Como você provavelmente sabe, && significa um booleano e (and), || significa um booleano ou (or). not nega um True ou um False.

ghci> True && False
False
ghci> True && True
True
ghci> False || True
True
ghci> not False
True
ghci> not (True && True)
False

O teste de igualdade é feito assim.

ghci> 5 == 5
True
ghci> 1 == 0
False
ghci> 5 /= 5
False
ghci> 5 /= 4
True
ghci> "hello" == "hello"
True

Que tal fazer 5 + "llama" ou 5 == True? Bem, se tentarmos o primeiro trecho, receberemos uma mensagem de erro grande e assustadora!

<interactive>:1:1: error: [GHC-39999]
    • No instance for ‘Num String’ arising from the literal ‘5’
    • In the first argument of ‘(+)’, namely ‘5’
      In the expression: 5 + "llama"
      In an equation for ‘it’: it = 5 + "llama"

Caramba! O que o GHCI está nos dizendo aqui é que "llama" não é um número e, portanto, não sabe como adicioná-lo a 5. Mesmo se não fosse "llama", mas "four" ou "4", Haskell ainda não consideraria isso um número. + espera que seu lado esquerdo e direito sejam números. Se tentássemos fazer True == 5, o GHCI nos diria que os tipos não correspondem. Considerando que + funciona apenas em coisas consideradas números, == funciona em quaisquer duas coisas que podem ser comparadas. Mas o problema é que ambos têm que ser o mesmo tipo de coisa. Você não pode comparar maçãs e laranjas. Vamos dar uma olhada nos tipos um pouco mais tarde. Nota: você pode fazer 5 + 4.0 porque 5 é sorrateiro e pode agir como um número inteiro ou um número de ponto flutuante. 4.0 não pode agir como um número inteiro, então 5 é quem tem que se adaptar.

Nota: Todos os erros do GHC recebem identificadores exclusivos, como GHC-39999 acima. Sempre que você estiver preso a um erro persistente, pode procurá-lo em https://errors.haskell.org/ para aprender causas típicas e soluções.

Você pode não saber, mas usamos funções o tempo todo. Por exemplo, * é uma função que pega dois números e os multiplica. Como você viu, nós a chamamos colocando-a entre eles. Isso é o que chamamos de função infixa. A maioria das funções que não são usadas com números são funções prefixas. Vamos dar uma olhada nelas.

phoen As funções geralmente são prefixas, portanto, a partir de agora, não declararemos explicitamente que uma função é da forma prefixa, apenas assumiremos. Na maioria das linguagens imperativas, as funções são chamadas escrevendo o nome da função e, em seguida, escrevendo seus parâmetros entre parênteses, geralmente separados por vírgulas. Em Haskell, as funções são chamadas escrevendo o nome da função, um espaço e depois os parâmetros, separados por espaços. Para começar, tentaremos chamar uma das funções mais chatas do Haskell.

ghci> succ 8
9

A função succ pega qualquer coisa que tenha um sucessor definido e retorna esse sucessor. Como você pode ver, apenas separamos o nome da função do parâmetro com um espaço. Chamar uma função com vários parâmetros também é simples. As funções min e max recebem duas coisas que podem ser colocadas em uma ordem (como números inteiros!). min retorna o que é menor e max retorna o que é maior. Veja você mesmo:

ghci> min 9 10
9
ghci> max 100 101
101

A aplicação de função (chamar uma função colocando um espaço depois dela e digitando os parâmetros) tem a maior precedência de todas. O que isso significa para nós é que essas duas declarações são equivalentes.

ghci> succ 9 + max 5 4 + 1
16
ghci> (succ 9) + (max 5 4) + 1
16

No entanto, se quiséssemos obter o sucessor do produto dos números 9 e 10, não poderíamos escrever succ 9 * 10 porque isso obteria o sucessor de 9, que seria multiplicado por 10. Então 100. Teríamos que escrever succ (9 * 10) para obter 91.

Se uma função recebe dois parâmetros, também podemos chamá-la como uma função infixa, cercando-a com crases. Por exemplo, a função div pega dois inteiros e faz a divisão integral entre eles. Fazer div 92 10 resulta em 9. Mas quando chamamos assim, pode haver alguma confusão sobre qual número está fazendo a divisão e qual está sendo dividido. Então, podemos chamá-la como uma função infixa fazendo 92 `div` 10 e de repente fica muito mais claro.

Muitas pessoas que vêm de linguagens imperativas tendem a manter a noção de que os parênteses devem denotar a aplicação de funções. Por exemplo, em C, você usa parênteses para chamar funções como foo(), bar(1) ou baz(3, "haha"). Como dissemos, espaços são usados para aplicação de funções em Haskell. Portanto, essas funções em Haskell seriam foo, bar 1 e baz 3 "haha". Portanto, se você vir algo como bar (bar 3), isso não significa que bar é chamado com bar e 3 como parâmetros. Isso significa que primeiro chamamos a função bar com 3 como parâmetro para obter algum número e depois chamamos bar novamente com esse número. Em C, isso seria algo como bar(bar(3)).

Primeiras funções do bebê

Na seção anterior, tivemos uma noção básica de como chamar funções. Agora vamos tentar fazer as nossas! Abra seu editor de texto favorito e digite esta função que pega um número e o multiplica por dois.

doubleMe x = x + x

As funções são definidas de maneira semelhante a como são chamadas. O nome da função é seguido por parâmetros separados por espaços. Mas, ao definir funções, há um = e depois definimos o que a função faz. Salve isso como baby.hs ou algo assim. Agora navegue até onde está salvo e execute o ghci a partir daí. Uma vez dentro do GHCI, faça :l baby. Agora que nosso script está carregado, podemos brincar com a função que definimos.

ghci> :l baby
[1 of 1] Compiling Main             ( baby.hs, interpreted )
Ok, one module loaded.
ghci> doubleMe 9
18
ghci> doubleMe 8.3
16.6

Como o + funciona em números inteiros e em números de ponto flutuante (qualquer coisa que possa ser considerada um número, na verdade), nossa função também funciona em qualquer número. Vamos fazer uma função que pega dois números e multiplica cada um por dois e depois os soma.

doubleUs x y = x*2 + y*2

Simples. Poderíamos também ter definido como doubleUs x y = x + x + y + y. Testá-lo produz resultados bastante previsíveis (lembre-se de anexar essa função ao arquivo baby.hs, salvá-lo e depois fazer :l baby dentro do GHCI).

ghci> doubleUs 4 9
26
ghci> doubleUs 2.3 34.2
73.0
ghci> doubleUs 28 88 + doubleMe 123
478

Como esperado, você pode chamar suas próprias funções de outras funções que você criou. Com isso em mente, poderíamos redefinir doubleUs assim:

doubleUs x y = doubleMe x + doubleMe y

Este é um exemplo muito simples de um padrão comum que você verá em Haskell. Fazer funções básicas que são obviamente corretas e combiná-las em funções mais complexas. Dessa forma, você também evita repetições. E se alguns matemáticos decidissem que 2 é na verdade 3 e você tivesse que mudar seu programa? Você poderia simplesmente redefinir doubleMe para ser x + x + x e, como doubleUs chama doubleMe, funcionaria automaticamente neste estranho mundo novo onde 2 é 3.

As funções em Haskell não precisam estar em nenhuma ordem específica, portanto, não importa se você define doubleMe primeiro e depois doubleUs ou se faz o contrário.

Agora vamos fazer uma função que multiplica um número por 2, mas apenas se esse número for menor ou igual a 100, porque números maiores que 100 já são grandes o suficiente!

doubleSmallNumber x = if x > 100
                        then x
                        else x*2

this is you

Bem aqui introduzimos a declaração if do Haskell. Você provavelmente está familiarizado com declarações if de outras linguagens. A diferença entre a declaração if do Haskell e as declarações if em linguagens imperativas é que a parte else é obrigatória no Haskell. Em linguagens imperativas, você pode pular algumas etapas se a condição não for satisfeita, mas em Haskell toda expressão e função deve retornar algo. Poderíamos ter escrito essa declaração if em uma linha, mas acho essa maneira mais legível. Outra coisa sobre a declaração if em Haskell é que ela é uma expressão. Uma expressão é basicamente um pedaço de código que retorna um valor. 5 é uma expressão porque retorna 5, 4 + 8 é uma expressão, x + y é uma expressão porque retorna a soma de x e y. Como o else é obrigatório, uma declaração if sempre retornará algo e é por isso que é uma expressão. Se quiséssemos adicionar um a cada número produzido em nossa função anterior, poderíamos ter escrito seu corpo assim.

doubleSmallNumber' x = (if x > 100 then x else x*2) + 1

Se tivéssemos omitido os parênteses, teria adicionado um apenas se x não fosse maior que 100. Observe o ' no final do nome da função. Esse apóstrofo não tem nenhum significado especial na sintaxe do Haskell. É um caractere válido para usar em um nome de função. Geralmente usamos ' para denotar uma versão estrita de uma função (uma que não é preguiçosa) ou uma versão ligeiramente modificada de uma função ou variável. Como ' é um caractere válido em funções, podemos fazer uma função como esta.

conanO'Brien = "It's a-me, Conan O'Brien!"

Há duas coisas dignas de nota aqui. A primeira é que no nome da função não colocamos o nome de Conan em maiúscula. Isso ocorre porque as funções não podem começar com letras maiúsculas. Veremos o porquê um pouco mais tarde. A segunda coisa é que essa função não recebe nenhum parâmetro. Quando uma função não recebe nenhum parâmetro, geralmente dizemos que é uma definição (ou um nome). Como não podemos mudar o que os nomes (e funções) significam depois de defini-los, conanO'Brien e a string "It's a-me, Conan O'Brien!" podem ser usados de forma intercambiável.

Uma introdução às listas

BUY A DOG Assim como listas de compras no mundo real, as listas em Haskell são muito úteis. É a estrutura de dados mais usada e pode ser usada de várias maneiras diferentes para modelar e resolver um monte de problemas. Listas são TÃO incríveis. Nesta seção, veremos o básico de listas, strings (que são listas) e compreensões de lista (list comprehensions).

Em Haskell, as listas são uma estrutura de dados homogênea. Elas armazenam vários elementos do mesmo tipo. Isso significa que podemos ter uma lista de números inteiros ou uma lista de caracteres, mas não podemos ter uma lista que tenha alguns números inteiros e depois alguns caracteres. E agora, uma lista!

ghci> lostNumbers = [4,8,15,16,23,42]
ghci> lostNumbers
[4,8,15,16,23,42]

Como você pode ver, as listas são denotadas por colchetes e os valores nas listas são separados por vírgulas. Se tentássemos uma lista como [1,2,'a',3,'b','c',4], Haskell reclamaria que caracteres (que são, a propósito, denotados como um caractere entre aspas simples) não são números. Falando em caracteres, strings são apenas listas de caracteres. "hello" é apenas açúcar sintático para ['h','e','l','l','o']. Como strings são listas, podemos usar funções de lista nelas, o que é muito útil.

Uma tarefa comum é juntar duas listas. Isso é feito usando o operador ++.

ghci> [1,2,3,4] ++ [9,10,11,12]
[1,2,3,4,9,10,11,12]
ghci> "hello" ++ " " ++ "world"
"hello world"
ghci> ['w','o'] ++ ['o','t']
"woot"

Cuidado ao usar repetidamente o operador ++ em strings longas. Quando você junta duas listas (mesmo se você anexar uma lista singleton a uma lista, por exemplo: [1,2,3] ++ [4]), internamente, Haskell tem que percorrer toda a lista do lado esquerdo de ++. Isso não é um problema ao lidar com listas que não são muito grandes. Mas colocar algo no final de uma lista com cinquenta milhões de entradas vai demorar um pouco. No entanto, colocar algo no início de uma lista usando o operador : (também chamado de operador cons) é instantâneo.

ghci> 'A':" SMALL CAT"
"A SMALL CAT"
ghci> 5:[1,2,3,4,5]
[5,1,2,3,4,5]

Observe como : recebe um número e uma lista de números ou um caractere e uma lista de caracteres, enquanto ++ recebe duas listas. Mesmo se você estiver adicionando um elemento ao final de uma lista com ++, deve cercá-lo com colchetes para que se torne uma lista.

[1,2,3] é na verdade apenas açúcar sintático para 1:2:3:[]. [] é uma lista vazia. Se precedermos 3 a ela, ela se tornará [3]. Se precedermos 2 a isso, ela se tornará [2,3], e assim por diante.

Nota: [], [[]] e [[],[],[]] são todas coisas diferentes. O primeiro é uma lista vazia, o segundo é uma lista que contém uma lista vazia, o terceiro é uma lista que contém três listas vazias.

Se você quiser obter um elemento de uma lista por índice, use !!. Os índices começam em 0.

ghci> "Steve Buscemi" !! 6
'B'
ghci> [9.4,33.2,96.2,11.2,23.25] !! 1
33.2

Mas se você tentar obter o sexto elemento de uma lista que tem apenas quatro elementos, receberá um erro, então tenha cuidado!

Listas também podem conter listas. Elas também podem conter listas que contêm listas que contêm listas…

ghci> b = [[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3]]
ghci> b
[[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3]]
ghci> b ++ [[1,1,1,1]]
[[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3],[1,1,1,1]]
ghci> [6,6,6]:b
[[6,6,6],[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3]]
ghci> b !! 2
[1,2,2,3,4]

As listas dentro de uma lista podem ter comprimentos diferentes, mas não podem ser de tipos diferentes. Assim como você não pode ter uma lista que tenha alguns caracteres e alguns números, você não pode ter uma lista que tenha algumas listas de caracteres e algumas listas de números.

As listas podem ser comparadas se as coisas que elas contêm puderem ser comparadas. Ao usar <, <=, > e >= para comparar listas, elas são comparadas em ordem lexicográfica. Primeiro, as cabeças (heads) são comparadas. Se forem iguais, os segundos elementos são comparados, etc.

ghci> [3,2,1] > [2,1,0]
True
ghci> [3,2,1] > [2,10,100]
True
ghci> [3,4,2] > [3,4]
True
ghci> [3,4,2] > [2,4]
True
ghci> [3,4,2] == [3,4,2]
True

O que mais você pode fazer com listas? Aqui estão algumas funções básicas que operam em listas.

head pega uma lista e retorna sua cabeça (head). A cabeça de uma lista é basicamente seu primeiro elemento.

ghci> head [5,4,3,2,1]
5

tail pega uma lista e retorna sua cauda (tail). Em outras palavras, corta a cabeça de uma lista.

ghci> tail [5,4,3,2,1]
[4,3,2,1]

last pega uma lista e retorna seu último elemento.

ghci> last [5,4,3,2,1]
1

init pega uma lista e retorna tudo, exceto seu último elemento.

ghci> init [5,4,3,2,1]
[5,4,3,2]

Se pensarmos em uma lista como um monstro, aqui está o que é o quê.

list monster

Mas o que acontece se tentarmos pegar a cabeça de uma lista vazia?

ghci> head []
*** Exception: Prelude.head: empty list

Oh céus! Tudo explode na nossa cara! Se não há monstro, ele não tem cabeça. Ao usar head, tail, last e init, tenha cuidado para não usá-los em listas vazias. Esse erro não pode ser detectado em tempo de compilação, portanto, é sempre uma boa prática tomar precauções contra dizer acidentalmente a Haskell para fornecer alguns elementos de uma lista vazia.

length pega uma lista e retorna seu comprimento, obviamente.

ghci> length [5,4,3,2,1]
5

null verifica se uma lista está vazia. Se estiver, retorna True, caso contrário, retorna False. Use esta função em vez de xs == [] (se você tiver uma lista chamada xs).

ghci> null [1,2,3]
False
ghci> null []
True

reverse inverte uma lista.

ghci> reverse [5,4,3,2,1]
[1,2,3,4,5]

take pega um número e uma lista. Ele extrai essa quantidade de elementos do início da lista. Veja.

ghci> take 3 [5,4,3,2,1]
[5,4,3]
ghci> take 1 [3,9,3]
[3]
ghci> take 5 [1,2]
[1,2]
ghci> take 0 [6,6,6]
[]

Veja como, se tentarmos pegar mais elementos do que há na lista, ela apenas retorna a lista. Se tentarmos pegar 0 elementos, obtemos uma lista vazia.

drop funciona de maneira semelhante, apenas descarta o número de elementos do início de uma lista.

ghci> drop 3 [8,4,2,1,5,6]
[1,5,6]
ghci> drop 0 [1,2,3,4]
[1,2,3,4]
ghci> drop 100 [1,2,3,4]
[]

maximum pega uma lista de coisas que podem ser colocadas em algum tipo de ordem e retorna o maior elemento.

minimum retorna o menor.

ghci> minimum [8,4,2,1,5,6]
1
ghci> maximum [1,9,2,3,4]
9

sum pega uma lista de números e retorna sua soma.

product pega uma lista de números e retorna seu produto.

ghci> sum [5,2,1,6,3,2,5,7]
31
ghci> product [6,2,1,2]
24
ghci> product [1,2,5,6,7,9,2,0]
0

elem pega uma coisa e uma lista de coisas e nos diz se essa coisa é um elemento da lista. Geralmente é chamado como uma função infixa porque é mais fácil de ler dessa maneira.

ghci> 4 `elem` [3,4,5,6]
True
ghci> 10 `elem` [3,4,5,6]
False

Essas foram algumas funções básicas que operam em listas. Vamos dar uma olhada em mais funções de lista mais tarde.

Intervalos do Texas (Texas ranges)

draw E se quisermos uma lista de todos os números entre 1 e 20? Claro, poderíamos digitar todos eles, mas obviamente isso não é uma solução para cavalheiros que exigem excelência de suas linguagens de programação. Em vez disso, usaremos intervalos (ranges). Os intervalos são uma maneira de criar listas que são sequências aritméticas de elementos que podem ser enumerados. Os números podem ser enumerados. Um, dois, três, quatro, etc. Os caracteres também podem ser enumerados. O alfabeto é uma enumeração de caracteres de A a Z. Nomes não podem ser enumerados. O que vem depois de “John”? Eu não sei.

Para fazer uma lista contendo todos os números naturais de 1 a 20, basta escrever [1..20]. Isso é o equivalente a escrever [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20] e não há diferença entre escrever um ou outro, exceto que escrever longas sequências de enumeração manualmente é estúpido.

ghci> [1..20]
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
ghci> ['a'..'z']
"abcdefghijklmnopqrstuvwxyz"
ghci> ['K'..'Z']
"KLMNOPQRSTUVWXYZ"

Os intervalos são legais porque você também pode especificar um passo. E se quisermos todos os números pares entre 1 e 20? Ou a cada terceiro número entre 1 e 20?

ghci> [2,4..20]
[2,4,6,8,10,12,14,16,18,20]
ghci> [3,6..20]
[3,6,9,12,15,18]

É simplesmente uma questão de separar os dois primeiros elementos com uma vírgula e depois especificar qual é o limite superior. Embora bastante inteligentes, os intervalos com passos não são tão inteligentes quanto algumas pessoas esperam que sejam. Você não pode fazer [1,2,4,8,16..100] e esperar obter todas as potências de 2. Primeiro, porque você só pode especificar um passo. E segundo, porque algumas sequências que não são aritméticas são ambíguas se dadas apenas por alguns de seus primeiros termos.

Para fazer uma lista com todos os números de 20 a 1, você não pode simplesmente fazer [20..1], você tem que fazer [20,19..1].

Cuidado ao usar números de ponto flutuante em intervalos! Como eles não são completamente precisos (por definição), seu uso em intervalos pode gerar alguns resultados bastante estranhos.

ghci> [0.1, 0.3 .. 1]
[0.1,0.3,0.5,0.7,0.8999999999999999,1.0999999999999999]

Meu conselho é não usá-los em intervalos de lista.

Você também pode usar intervalos para fazer listas infinitas simplesmente não especificando um limite superior. Mais tarde, entraremos em mais detalhes sobre listas infinitas. Por enquanto, vamos examinar como você obteria os primeiros 24 múltiplos de 13. Claro, você poderia fazer [13,26..24*13]. Mas há uma maneira melhor: take 24 [13,26..]. Como Haskell é preguiçoso, ele não tentará avaliar a lista infinita imediatamente, porque nunca terminaria. Ele vai esperar para ver o que você quer tirar dessas listas infinitas. E aqui ele vê que você quer apenas os primeiros 24 elementos e ele concorda com prazer.

Um punhado de funções que produzem listas infinitas:

cycle pega uma lista e a alterna em uma lista infinita. Se você apenas tentar exibir o resultado, continuará para sempre, então você terá que cortá-lo em algum lugar.

ghci> take 10 (cycle [1,2,3])
[1,2,3,1,2,3,1,2,3,1]
ghci> take 12 (cycle "LOL ")
"LOL LOL LOL "

repeat pega um elemento e produz uma lista infinita apenas desse elemento. É como repetir uma lista com apenas um elemento.

ghci> take 10 (repeat 5)
[5,5,5,5,5,5,5,5,5,5]

Embora seja mais simples usar a função replicate se você quiser algum número do mesmo elemento em uma lista. replicate 3 10 retorna [10,10,10].

Eu sou uma compreensão de lista (list comprehension)

frog Se você já fez um curso de matemática, provavelmente já se deparou com compreensões de conjunto. Elas são normalmente usadas para construir conjuntos mais específicos a partir de conjuntos gerais. Uma compreensão básica para um conjunto que contém os primeiros dez números naturais pares é set notation. A parte antes do pipe é chamada de função de saída, x é a variável, N é o conjunto de entrada e x <= 10 é o predicado. Isso significa que o conjunto contém os dobros de todos os números naturais que satisfazem o predicado.

Se quiséssemos escrever isso em Haskell, poderíamos fazer algo como take 10 [2,4..]. Mas e se não quiséssemos dobros dos primeiros 10 números naturais, mas algum tipo de função mais complexa aplicada a eles? Poderíamos usar uma compreensão de lista para isso. As compreensões de lista são muito semelhantes às compreensões de conjunto. Vamos nos ater a obter os primeiros 10 números pares por enquanto. A compreensão de lista que poderíamos usar é [x*2 | x <- [1..10]]. x é extraído de [1..10] e para cada elemento em [1..10] (que vinculamos a x), obtemos esse elemento, apenas dobrado. Aqui está essa compreensão em ação.

ghci> [x*2 | x <- [1..10]]
[2,4,6,8,10,12,14,16,18,20]

Como você pode ver, obtemos os resultados desejados. Agora vamos adicionar uma condição (ou um predicado) a essa compreensão. Os predicados vêm depois das partes de ligação e são separados delas por uma vírgula. Digamos que queremos apenas os elementos que, dobrados, são maiores ou iguais a 12.

ghci> [x*2 | x <- [1..10], x*2 >= 12]
[12,14,16,18,20]

Legal, funciona. Que tal se quiséssemos todos os números de 50 a 100 cujo resto quando dividido pelo número 7 é 3? Fácil.

ghci> [ x | x <- [50..100], x `mod` 7 == 3]
[52,59,66,73,80,87,94]

Sucesso! Observe que remover listas por predicados também é chamado de filtragem. Pegamos uma lista de números e os filtramos pelo predicado. Agora, outro exemplo. Digamos que queremos uma compreensão que substitua cada número ímpar maior que 10 por "BANG!" e cada número ímpar menor que 10 por "BOOM!". Se um número não for ímpar, nós o jogamos fora da nossa lista. Por conveniência, colocaremos essa compreensão dentro de uma função para que possamos reutilizá-la facilmente.

boomBangs xs = [ if x < 10 then "BOOM!" else "BANG!" | x <- xs, odd x]

A última parte da compreensão é o predicado. A função odd retorna True em um número ímpar e False em um par. O elemento é incluído na lista somente se todos os predicados forem avaliados como True.

ghci> boomBangs [7..13]
["BOOM!","BOOM!","BANG!","BANG!"]

Podemos incluir vários predicados. Se quiséssemos todos os números de 10 a 20 que não são 13, 15 ou 19, faríamos:

ghci> [ x | x <- [10..20], x /= 13, x /= 15, x /= 19]
[10,11,12,14,16,17,18,20]

Não apenas podemos ter vários predicados em compreensões de lista (um elemento deve satisfazer todos os predicados para ser incluído na lista resultante), também podemos extrair de várias listas. Ao extrair de várias listas, as compreensões produzem todas as combinações das listas fornecidas e, em seguida, as unem pela função de saída que fornecemos. Uma lista produzida por uma compreensão que extrai de duas listas de comprimento 4 terá um comprimento de 16, desde que não as filtremos. Se tivermos duas listas, [2,5,10] e [8,10,11] e quisermos obter os produtos de todas as combinações possíveis entre números nessas listas, eis o que faríamos.

ghci> [ x*y | x <- [2,5,10], y <- [8,10,11]]
[16,20,22,40,50,55,80,100,110]

Como esperado, o comprimento da nova lista é 9. E se quiséssemos todos os produtos possíveis com mais de 50?

ghci> [ x*y | x <- [2,5,10], y <- [8,10,11], x*y > 50]
[55,80,100,110]

Que tal uma compreensão de lista que combina uma lista de adjetivos e uma lista de substantivos… para uma hilaridade épica.

ghci> nouns = ["hobo","frog","pope"]
ghci> adjectives = ["lazy","grouchy","scheming"]
ghci> [adjective ++ " " ++ noun | adjective <- adjectives, noun <- nouns]
["lazy hobo","lazy frog","lazy pope","grouchy hobo","grouchy frog",
"grouchy pope","scheming hobo","scheming frog","scheming pope"]

Eu sei! Vamos escrever nossa própria versão de length! Vamos chamá-la de length'.

length' xs = sum [1 | _ <- xs]

_ significa que não nos importamos com o que extrairemos da lista de qualquer maneira; portanto, em vez de escrever um nome de variável que nunca usaremos, apenas escrevemos _. Essa função substitui cada elemento de uma lista por 1 e depois soma tudo. Isso significa que a soma resultante será o comprimento da nossa lista.

Apenas um lembrete amigável: como as strings são listas, podemos usar compreensões de lista para processar e produzir strings. Aqui está uma função que pega uma string e remove tudo, exceto letras maiúsculas.

removeNonUppercase st = [ c | c <- st, c `elem` ['A'..'Z']]

Testando:

ghci> removeNonUppercase "Hahaha! Ahahaha!"
"HA"
ghci> removeNonUppercase "IdontLIKEFROGS"
"ILIKEFROGS"

O predicado aqui faz todo o trabalho. Diz que o caractere será incluído na nova lista apenas se for um elemento da lista ['A'..'Z']. Compreensões de lista aninhadas também são possíveis se você estiver operando em listas que contêm listas. Uma lista contém várias listas de números. Vamos remover todos os números ímpares sem achatar a lista.

ghci> let xxs = [[1,3,5,2,3,1,2,4,5],[1,2,3,4,5,6,7,8,9],[1,2,4,2,1,6,3,1,3,2,3,6]]
ghci> [ [ x | x <- xs, even x ] | xs <- xxs]
[[2,2,4],[2,4,6,8],[2,4,2,6,2,6]]

Você pode escrever compreensões de lista em várias linhas. Portanto, se você não estiver no GHCI, é melhor dividir compreensões de lista mais longas em várias linhas, especialmente se estiverem aninhadas.

Tuplas

tuples

De certa forma, as tuplas são como listas — são uma maneira de armazenar vários valores em um único valor. No entanto, existem algumas diferenças fundamentais. Uma lista de números é uma lista de números. Esse é o seu tipo e não importa se tem apenas um número ou uma quantidade infinita de números. As tuplas, no entanto, são usadas quando você sabe exatamente quantos valores deseja combinar e seu tipo depende de quantos componentes ela tem e dos tipos dos componentes. Elas são denotadas com parênteses e seus componentes são separados por vírgulas.

Outra diferença fundamental é que elas não precisam ser homogêneas. Ao contrário de uma lista, uma tupla pode conter uma combinação de vários tipos.

Pense em como representaríamos um vetor bidimensional em Haskell. Uma maneira seria usar uma lista. Isso meio que funcionaria. E se quiséssemos colocar alguns vetores em uma lista para representar pontos de uma forma em um plano bidimensional? Poderíamos fazer algo como [[1,2],[8,11],[4,5]]. O problema com esse método é que também poderíamos fazer coisas como [[1,2],[8,11,5],[4,5]], com o que Haskell não tem problema, pois ainda é uma lista de listas com números, mas meio que não faz sentido. Mas uma tupla de tamanho dois (também chamada de par) é seu próprio tipo, o que significa que uma lista não pode ter alguns pares nela e depois um trio (uma tupla de tamanho três), então vamos usar isso. Em vez de cercar os vetores com colchetes, usamos parênteses: [(1,2),(8,11),(4,5)]. E se tentássemos fazer uma forma como [(1,2),(8,11,5),(4,5)]? Bem, teríamos este erro:

<interactive>:1:8: error: [GHC-83865]
    • Couldn't match expected type: (a, b)
                  with actual type: (a0, b0, c0)
    • In the expression: (8, 11, 5)
      In the expression: [(1, 2), (8, 11, 5), (4, 5)]
      In an equation for ‘it’: it = [(1, 2), (8, 11, 5), (4, 5)]
    • Relevant bindings include
        it :: [(a, b)] (bound at <interactive>:1:1)

Ele está nos dizendo que tentamos usar um par e um trio na mesma lista, o que não deveria acontecer. Você também não poderia fazer uma lista como [(1,2),("One",2)] porque o primeiro elemento da lista é um par de números e o segundo elemento é um par consistindo de uma string e um número. As tuplas também podem ser usadas para representar uma grande variedade de dados. Por exemplo, se quiséssemos representar o nome e a idade de alguém em Haskell, poderíamos usar um trio: ("Christopher", "Walken", 55). Como visto neste exemplo, as tuplas também podem conter listas.

Use tuplas quando souber com antecedência quantos componentes algum dado deve ter. As tuplas são muito mais rígidas porque cada tamanho diferente de tupla é seu próprio tipo, então você não pode escrever uma função geral para anexar um elemento a uma tupla — você teria que escrever uma função para anexar a um par, uma função para anexar a um trio, uma função para anexar a uma 4-tupla, etc.

Embora existam listas singleton, não existe tupla singleton. Na verdade, não faz muito sentido quando você pensa sobre isso. Uma tupla singleton seria apenas o valor que ela contém e, como tal, não teria nenhum benefício para nós.

Como listas, as tuplas podem ser comparadas entre si se seus componentes puderem ser comparados. Só que você não pode comparar duas tuplas de tamanhos diferentes, enquanto pode comparar duas listas de tamanhos diferentes. Duas funções úteis que operam em pares:

fst pega um par e retorna seu primeiro componente.

ghci> fst (8,11)
8
ghci> fst ("Wow", False)
"Wow"

snd pega um par e retorna seu segundo componente. Surpresa!

ghci> snd (8,11)
11
ghci> snd ("Wow", False)
False

Nota: essas funções operam apenas em pares. Elas não funcionarão em trios, 4-tuplas, 5-tuplas, etc. Veremos a extração de dados de tuplas de maneiras diferentes um pouco mais tarde.

Uma função legal que produz uma lista de pares: zip. Ela pega duas listas e as compacta em uma lista unindo os elementos correspondentes em pares. É uma função muito simples, mas tem muitos usos. É especialmente útil para quando você deseja combinar duas listas de uma maneira ou percorrer duas listas simultaneamente. Aqui está uma demonstração.

ghci> zip [1,2,3,4,5] [5,5,5,5,5]
[(1,5),(2,5),(3,5),(4,5),(5,5)]
ghci> zip [1 .. 5] ["one", "two", "three", "four", "five"]
[(1,"one"),(2,"two"),(3,"three"),(4,"four"),(5,"five")]

Ela emparelha os elementos e produz uma nova lista. O primeiro elemento vai com o primeiro, o segundo com o segundo, etc. Observe que, como os pares podem ter tipos diferentes neles, o zip pode pegar duas listas que contêm tipos diferentes e compactá-las. O que acontece se os comprimentos das listas não corresponderem?

ghci> zip [5,3,2,6,2,7,2,5,4,6,6] ["im","a","turtle"]
[(5,"im"),(3,"a"),(2,"turtle")]

A lista mais longa é simplesmente cortada para corresponder ao comprimento da mais curta. Como Haskell é preguiçoso, podemos compactar listas finitas com listas infinitas:

ghci> zip [1..] ["apple", "orange", "cherry", "mango"]
[(1,"apple"),(2,"orange"),(3,"cherry"),(4,"mango")]

look at meee

Aqui está um problema que combina tuplas e compreensões de lista: qual triângulo retângulo que tem inteiros para todos os lados e todos os lados iguais ou menores que 10 tem um perímetro de 24? Primeiro, vamos tentar gerar todos os triângulos com lados iguais ou menores a 10:

ghci> triangles = [ (a,b,c) | c <- [1..10], b <- [1..10], a <- [1..10] ]

Estamos apenas extraindo de três listas e nossa função de saída as está combinando em um trio. Se você avaliar isso digitando triangles no GHCI, obterá uma lista de todos os triângulos possíveis com lados menores ou iguais a 10. Em seguida, adicionaremos uma condição de que todos eles devem ser triângulos retângulos. Também modificaremos essa função levando em consideração que o lado b não é maior que a hipotenusa e que o lado a não é maior que o lado b.

ghci> rightTriangles = [ (a,b,c) | c <- [1..10], b <- [1..c], a <- [1..b], a^2 + b^2 == c^2]

Estamos quase terminando. Agora, apenas modificamos a função dizendo que queremos aqueles em que o perímetro é 24.

ghci> rightTriangles' = [ (a,b,c) | c <- [1..10], b <- [1..c], a <- [1..b], a^2 + b^2 == c^2, a+b+c == 24]
ghci> rightTriangles'
[(6,8,10)]

E aí está a nossa resposta! Esse é um padrão comum na programação funcional. Você pega um conjunto inicial de soluções e depois aplica transformações a essas soluções e as filtra até obter as certas.