Módulos (Modules)

Um módulo Haskell é uma coleção de funções, tipos e typeclasses relacionados. Um programa Haskell é uma coleção de módulos onde o módulo principal carrega os outros módulos e, em seguida, usa as funções definidas neles para fazer algo. Ter o código dividido em vários módulos tem muitas vantagens. Se um módulo for genérico o suficiente, as funções que ele exporta podem ser usadas em uma infinidade de programas diferentes. Se o seu próprio código for separado em módulos independentes que não dependem muito uns dos outros (também dizemos que eles são fracamente acoplados), você poderá reutilizá-los mais tarde. Isso torna todo o negócio de escrever código mais gerenciável, dividindo-o em várias partes, cada uma das quais tem algum tipo de propósito.
A biblioteca padrão do Haskell é dividida em módulos, cada um deles
contém funções e tipos que estão de alguma forma relacionados e servem a
algum propósito comum. Há um módulo para manipular listas, um módulo
para programação concorrente, um módulo para lidar com números
complexos, etc. Todas as funções, tipos e typeclasses com os quais
lidamos até agora faziam parte do módulo Prelude, que é
importado por padrão. Neste capítulo, vamos examinar alguns módulos
úteis e as funções que eles têm. Mas primeiro, vamos ver como importar
módulos.
Carregando módulos (Loading modules)
A sintaxe para importar módulos em um script Haskell é
import <nome do módulo>. Isso deve ser feito antes de
definir quaisquer funções, portanto, as importações geralmente são
feitas no topo do arquivo. Um script pode, é claro, importar vários
módulos. Basta colocar cada instrução de importação em uma linha
separada. Vamos importar o módulo Data.List, que tem um
monte de funções úteis para trabalhar com listas e usar uma função que
ele exporta para criar uma função que nos diz quantos elementos únicos
uma lista tem.
import Data.List
numUniques :: (Eq a) => [a] -> Int
numUniques = length . nub
Quando você faz import Data.List, todas as funções que
Data.List exporta tornam-se disponíveis no namespace
global, o que significa que você pode chamá-las de qualquer lugar no
script. nub é uma função definida em Data.List
que pega uma lista e elimina elementos duplicados. Compor
length e nub fazendo length . nub
produz uma função que é o equivalente a
\xs -> length (nub xs).
Você também pode colocar as funções de módulos no namespace global ao
usar o GHCI. Se você estiver no GHCI e quiser chamar as funções
exportadas por Data.List, faça isso:
ghci> :m + Data.List
Se quisermos carregar os nomes de vários módulos dentro do GHCI, não
precisamos fazer :m + várias vezes, podemos apenas carregar
vários módulos de uma vez.
ghci> :m + Data.List Data.Map Data.Set
No entanto, se você carregou um script que já importa um módulo, não
precisa usar :m + para obter acesso a ele.
Se você precisar apenas de algumas funções de um módulo, poderá
importar seletivamente apenas essas funções. Se quiséssemos importar
apenas as funções nub e sort de
Data.List, faríamos isso:
import Data.List (nub, sort)
Você também pode optar por importar todas as funções de um módulo,
exceto algumas selecionadas. Isso geralmente é útil quando vários
módulos exportam funções com o mesmo nome e você deseja se livrar das
ofensivas. Digamos que já temos nossa própria função chamada
nub e queremos importar todas as funções de
Data.List, exceto a função nub:
import Data.List hiding (nub)
Outra maneira de lidar com conflitos de nomes é fazer importações
qualificadas. O módulo Data.Map, que oferece uma estrutura
de dados para pesquisar valores por chave, exporta um monte de funções
com o mesmo nome que as funções do Prelude, como
filter ou null. Portanto, quando importamos
Data.Map e chamamos filter, o Haskell não
saberá qual função usar. Veja como resolvemos isso:
import qualified Data.Map
Isso faz com que, se quisermos referenciar a função
filter de Data.Map, tenhamos que fazer
Data.Map.filter, enquanto apenas filter ainda
se refere ao filter normal que todos conhecemos e amamos.
Mas digitar Data.Map na frente de cada função desse módulo
é meio tedioso. É por isso que podemos renomear a importação qualificada
para algo mais curto:
import qualified Data.Map as M
Agora, para referenciar a função filter de
Data.Map, apenas usamos M.filter.
Use esta referência útil para ver quais módulos estão na biblioteca padrão. Uma ótima maneira de obter novos conhecimentos de Haskell é simplesmente clicar na referência da biblioteca padrão e explorar os módulos e suas funções. Você também pode visualizar o código-fonte do Haskell para cada módulo. Ler o código-fonte de alguns módulos é uma maneira muito boa de aprender Haskell e ter uma noção sólida dele.
Para procurar funções ou descobrir onde estão localizadas, use o Hoogle. É um mecanismo de pesquisa Haskell realmente incrível, você pode pesquisar por nome, nome do módulo ou até assinatura de tipo.
Data.List
O módulo Data.List é tudo sobre listas, obviamente. Ele
fornece algumas funções muito úteis para lidar com elas. Já conhecemos
algumas de suas funções (como map e filter)
porque o módulo Prelude exporta algumas funções de
Data.List por conveniência. Você não precisa importar
Data.List por meio de uma importação qualificada porque não
entra em conflito com nenhum nome do Prelude, exceto
aqueles que o Prelude já rouba de Data.List.
Vamos dar uma olhada em algumas das funções que não conhecemos
antes.
intersperse pega um elemento e
uma lista e, em seguida, coloca esse elemento entre cada par de
elementos na lista. Aqui está uma demonstração:
ghci> intersperse '.' "MONKEY"
"M.O.N.K.E.Y"
ghci> intersperse 0 [1,2,3,4,5,6]
[1,0,2,0,3,0,4,0,5,0,6]
intercalate pega uma lista e uma
lista de listas. Ele então insere essa lista entre todas essas listas e
depois nivela o resultado.
ghci> intercalate " " ["hey","there","folks"]
"hey there folks"
ghci> intercalate [0,0,0] [[1,2,3],[4,5,6],[7,8,9]]
[1,2,3,0,0,0,4,5,6,0,0,0,7,8,9]
transpose transpõe uma lista de
listas. Se você olhar para uma lista de listas como uma matriz 2D, as
colunas se tornarão as linhas e vice-versa.
ghci> transpose [[1,2,3],[4,5,6],[7,8,9]]
[[1,4,7],[2,5,8],[3,6,9]]
ghci> transpose ["hey","there","folks"]
["htf","eho","yel","rk","es"]
Digamos que temos os polinômios 3x2 + 5x + 9,
10x3 + 9 e 8x3 + 5x2 + x -
1 e queremos somá-los. Podemos usar as listas
[0,3,5,9], [10,0,0,9] e
[8,5,1,-1] para representá-los em Haskell. Agora, para
adicioná-los, tudo o que temos a fazer é isso:
ghci> map sum $ transpose [[0,3,5,9],[10,0,0,9],[8,5,1,-1]]
[18,8,6,17]
Quando transpusemos essas três listas, as terceiras potências estão
na primeira linha, as segundas potências na segunda e assim por diante.
Mapear sum para isso produz nosso resultado desejado.

foldl' e foldl1' são versões mais estritas de suas
respectivas encarnações preguiçosas. Ao usar dobras preguiçosas em
listas realmente grandes, você pode frequentemente obter um erro de
estouro de pilha (stack overflow error). O culpado por isso é que,
devido à natureza preguiçosa das dobras, o valor do acumulador não é
realmente atualizado à medida que a dobra acontece. O que realmente
acontece é que o acumulador faz uma promessa de que calculará seu valor
quando solicitado a realmente produzir o resultado (também chamado de
thunk). Isso acontece para cada acumulador intermediário e todos esses
thunks estouram sua pilha. As dobras estritas não são preguiçosas e
realmente calculam os valores intermediários à medida que avançam, em
vez de encher sua pilha com thunks. Portanto, se você receber erros de
estouro de pilha ao fazer dobras preguiçosas, tente mudar para suas
versões estritas.
concat nivela uma lista de listas
em apenas uma lista de elementos.
ghci> concat ["foo","bar","car"]
"foobarcar"
ghci> concat [[3,4,5],[2,3,4],[2,1,1]]
[3,4,5,2,3,4,2,1,1]
Ele removerá apenas um nível de aninhamento. Portanto, se você deseja
nivelar completamente [[[2,3],[3,4,5],[2]],[[2,3],[3,4]]],
que é uma lista de listas de listas, você deve concatená-la duas
vezes.
Fazer concatMap é o mesmo que
primeiro mapear uma função para uma lista e depois concatenar a lista
com concat.
ghci> concatMap (replicate 4) [1..3]
[1,1,1,1,2,2,2,2,3,3,3,3]
and pega uma lista de valores
booleanos e retorna True apenas se todos os valores na
lista forem True.
ghci> and $ map (>4) [5,6,7,8]
True
ghci> and $ map (==4) [4,4,4,3,4]
False
or é como and, só
que retorna True se algum dos valores booleanos em uma
lista for True.
ghci> or $ map (==4) [2,3,4,5,6,1]
True
ghci> or $ map (>4) [1,2,3]
False
any e all pegam um predicado e verificam se
algum ou todos os elementos de uma lista satisfazem o predicado,
respectivamente. Geralmente usamos essas duas funções em vez de mapear
sobre uma lista e depois fazer and ou or.
ghci> any (==4) [2,3,5,6,1,4]
True
ghci> all (>4) [6,9,10]
True
ghci> all (`elem` ['A'..'Z']) "HEYGUYSwhatsup"
False
ghci> any (`elem` ['A'..'Z']) "HEYGUYSwhatsup"
True
iterate pega uma função e um
valor inicial. Ele aplica a função ao valor inicial, depois aplica essa
função ao resultado, depois aplica a função a esse resultado novamente,
etc. Ele retorna todos os resultados na forma de uma lista infinita.
ghci> take 10 $ iterate (*2) 1
[1,2,4,8,16,32,64,128,256,512]
ghci> take 3 $ iterate (++ "haha") "haha"
["haha","hahahaha","hahahahahaha"]
splitAt pega um número e uma
lista. Ele então divide a lista em muitos elementos, retornando as duas
listas resultantes em uma tupla.
ghci> splitAt 3 "heyman"
("hey","man")
ghci> splitAt 100 "heyman"
("heyman","")
ghci> splitAt (-3) "heyman"
("","heyman")
ghci> let (a,b) = splitAt 3 "foobar" in b ++ a
"barfoo"
takeWhile é uma pequena função
realmente útil. Ela pega elementos de uma lista enquanto o predicado se
mantém e, quando um elemento é encontrado que não satisfaz o predicado,
é cortado. Acontece que isso é muito útil.
ghci> takeWhile (>3) [6,5,4,3,2,1,2,3,4,5,4,3,2,1]
[6,5,4]
ghci> takeWhile (/=' ') "This is a sentence"
"This"
Digamos que queríamos saber a soma de todas as terceiras potências
que estão abaixo de 10.000. Não podemos mapear (^3) para
[1..], aplicar um filtro e depois tentar somar isso porque
filtrar uma lista infinita nunca termina. Você pode saber que todos os
elementos aqui são ascendentes, mas Haskell não. É por isso que podemos
fazer isso:
ghci> sum $ takeWhile (<10000) $ map (^3) [1..]
53361
Aplicamos (^3) a uma lista infinita e, uma vez que um
elemento superior a 10.000 é encontrado, a lista é cortada. Agora
podemos somar facilmente.
dropWhile é semelhante, apenas
descarta todos os elementos enquanto o predicado é verdadeiro. Quando o
predicado é igual a False, ele retorna o restante da lista.
Uma função extremamente útil e adorável!
ghci> dropWhile (/=' ') "This is a sentence"
" is a sentence"
ghci> dropWhile (<3) [1,2,2,2,3,4,5,4,3,2,1]
[3,4,5,4,3,2,1]
Recebemos uma lista que representa o valor de uma ação por data. A lista é composta por tuplas cujo primeiro componente é o valor da ação, o segundo é o ano, o terceiro é o mês e o quarto é a data. Queremos saber quando o valor das ações excedeu mil dólares!
ghci> let stock = [(994.4,2008,9,1),(995.2,2008,9,2),(999.2,2008,9,3),(1001.4,2008,9,4),(998.3,2008,9,5)]
ghci> head (dropWhile (\(val,y,m,d) -> val < 1000) stock)
(1001.4,2008,9,4)
span é meio que como
takeWhile, só que retorna um par de listas. A primeira
lista contém tudo o que a lista resultante de takeWhile
conteria se fosse chamada com o mesmo predicado e a mesma lista. A
segunda lista contém a parte da lista que teria sido descartada.
ghci> let (fw, rest) = span (/=' ') "This is a sentence" in "First word:" ++ fw ++ ", the rest:" ++ rest
"First word: This, the rest: is a sentence"
Considerando que span abrange a lista enquanto o
predicado é verdadeiro, break a
quebra quando o predicado é verdadeiro. Fazer break p é o
equivalente a fazer span (not . p).
ghci> break (==4) [1,2,3,4,5,6,7]
([1,2,3],[4,5,6,7])
ghci> span (/=4) [1,2,3,4,5,6,7]
([1,2,3],[4,5,6,7])
Ao usar break, a segunda lista no resultado começará com
o primeiro elemento que satisfaz o predicado.
sort simplesmente classifica uma
lista. O tipo dos elementos na lista deve fazer parte da typeclass
Ord, porque se os elementos de uma lista não puderem ser
colocados em algum tipo de ordem, a lista não poderá ser
classificada.
ghci> sort [8,5,3,2,1,6,4,2]
[1,2,2,3,4,5,6,8]
ghci> sort "This will be sorted soon"
" Tbdeehiillnooorssstw"
group pega uma lista e agrupa
elementos adjacentes em sublistas se eles forem iguais.
ghci> group [1,1,1,1,2,2,2,2,3,3,2,2,2,5,6,7]
[[1,1,1,1],[2,2,2,2],[3,3],[2,2,2],[5],[6],[7]]
Se classificarmos uma lista antes de agrupá-la, podemos descobrir quantas vezes cada elemento aparece na lista.
ghci> map (\l@(x:xs) -> (x,length l)) . group . sort $ [1,1,1,1,2,2,2,2,3,3,2,2,2,5,6,7]
[(1,4),(2,7),(3,2),(5,1),(6,1),(7,1)]
inits e tails são como init e
tail, apenas aplicam recursivamente isso a uma lista até
que não resta nada. Observe.
ghci> inits "w00t"
["","w","w0","w00","w00t"]
ghci> tails "w00t"
["w00t","00t","0t","t",""]
ghci> let w = "w00t" in zip (inits w) (tails w)
[("","w00t"),("w","00t"),("w0","0t"),("w00","t"),("w00t","")]
Vamos usar uma dobra para implementar a pesquisa em uma lista por uma sublista.
search :: (Eq a) => [a] -> [a] -> Bool
search needle haystack =
let nlen = length needle
in foldl (\acc x -> if take nlen x == needle then True else acc) False (tails haystack)
Primeiro, chamamos tails com a lista na qual estamos
pesquisando. Então passamos por cima de cada cauda e vemos se ela começa
com o que estamos procurando.
Com isso, na verdade, acabamos de fazer uma função que se comporta
como isInfixOf.
isInfixOf procura uma sublista dentro de uma lista e
retorna True se a sublista que estamos procurando estiver
em algum lugar dentro da lista de destino.
ghci> "cat" `isInfixOf` "im a cat burglar"
True
ghci> "Cat" `isInfixOf` "im a cat burglar"
False
ghci> "cats" `isInfixOf` "im a cat burglar"
False
isPrefixOf e isSuffixOf procuram uma sublista no início
e no final de uma lista, respectivamente.
ghci> "hey" `isPrefixOf` "hey there!"
True
ghci> "hey" `isPrefixOf` "oh hey there!"
False
ghci> "there!" `isSuffixOf` "oh hey there!"
True
ghci> "there!" `isSuffixOf` "oh hey there"
False
elem e notElem verificam se um elemento está ou
não dentro de uma lista.
partition pega uma lista e um
predicado e retorna um par de listas. A primeira lista no resultado
contém todos os elementos que satisfazem o predicado, a segunda contém
todos os que não satisfazem.
ghci> partition (`elem` ['A'..'Z']) "BOBsidneyMORGANeddy"
("BOBMORGAN","sidneyeddy")
ghci> partition (>3) [1,3,5,6,3,2,1,0,3,7]
([5,6,7],[1,3,3,2,1,0,3])
É importante entender como isso é diferente de span e
break:
ghci> span (`elem` ['A'..'Z']) "BOBsidneyMORGANeddy"
("BOB","sidneyMORGANeddy")
Enquanto span e break terminam quando
encontram o primeiro elemento que não satisfaz e satisfaz o predicado,
partition passa por toda a lista e a divide de acordo com o
predicado.
find pega uma lista e um
predicado e retorna o primeiro elemento que satisfaz o predicado. Mas
retorna esse elemento envolvido em um valor Maybe.
Estaremos cobrindo tipos de dados algébricos mais aprofundadamente no
próximo capítulo, mas, por enquanto, é isso que você precisa saber: um
valor Maybe pode ser Just something ou
Nothing. Assim como uma lista pode ser uma lista vazia ou
uma lista com alguns elementos, um valor Maybe pode ser
nenhum elemento ou um único elemento. E como o tipo de uma lista de,
digamos, inteiros é [Int], o tipo de maybe ter um inteiro é
Maybe Int. De qualquer forma, vamos testar nossa função
find.
ghci> find (>4) [1,2,3,4,5,6]
Just 5
ghci> find (>9) [1,2,3,4,5,6]
Nothing
ghci> :t find
find :: (a -> Bool) -> [a] -> Maybe a
Observe o tipo de find. Seu resultado é
Maybe a. É como ter o tipo de [a], apenas um
valor do tipo Maybe pode conter nenhum elemento ou um
elemento, enquanto uma lista pode conter nenhum elemento, um elemento ou
vários elementos.
Lembre -se de quando estávamos procurando pela primeira vez que
nossas ações ultrapassaram US $ 1.000. Fizemos
head (dropWhile (\(val,y,m,d) -> val < 1000) stock).
Lembre-se de que head não é realmente seguro. O que
aconteceria se o nosso estoque nunca ultrapassasse US $ 1000? Nossa
aplicação de dropWhile retornaria uma lista vazia e obter o
head de uma lista vazia resultaria em um erro. No entanto, se
reescrevêssemos isso como
find (\(val,y,m,d) -> val > 1000) stock, estaríamos
muito mais seguros. Se nossas ações nunca ultrapassassem US $ 1000
(portanto, se nenhum elemento satisfaz o predicado), receberíamos um
Nothing. Mas se houvesse uma resposta válida nessa lista,
receberíamos, digamos, Just (1001.4,2008,9,4).
elemIndex é meio que como
elem, só que não retorna um valor booleano. Talvez retorne
o índice do elemento que estamos procurando. Se esse elemento não
estiver em nossa lista, ele retornará Nothing.
ghci> :t elemIndex
elemIndex :: (Eq a) => a -> [a] -> Maybe Int
ghci> 4 `elemIndex` [1,2,3,4,5,6]
Just 3
ghci> 10 `elemIndex` [1,2,3,4,5,6]
Nothing
elemIndices é como
elemIndex, só que retorna uma lista de índices, caso o
elemento que estamos procurando apareça na nossa lista várias vezes.
Como estamos usando uma lista para representar os índices, não
precisamos de um tipo Maybe, porque a falha pode ser
representada como a lista vazia, que é muito sinônimo de
Nothing.
ghci> ' ' `elemIndices` "Where are the spaces?"
[5,9,13]
findIndex é como find, mas talvez
retorne o índice do primeiro elemento que satisfaz o predicado. findIndices retorna os índices de todos os
elementos que satisfazem o predicado na forma de uma lista.
ghci> findIndex (==4) [5,3,2,1,6,4]
Just 5
ghci> findIndex (==7) [5,3,2,1,6,4]
Nothing
ghci> findIndices (`elem` ['A'..'Z']) "Where Are The Caps?"
[0,6,10,14]
Já cobrimos zip e zipWith. Observamos que
eles compactam duas listas, em uma tupla ou com uma função binária (o
que significa uma função que aceita dois parâmetros). Mas e se quisermos
compactar três listas? Ou compactar três listas com uma função que
aceita três parâmetros? Bem, para isso, temos zip3, zip4, etc. e zipWith3, zipWith4, etc. Essas variantes vão até 7.
Embora isso possa parecer uma gambiarra, funciona muito bem, porque não
há muitas vezes em que você deseja compactar 8 listas juntas. Também
existe uma maneira muito inteligente de compactar números infinitos de
listas, mas não somos avançados o suficiente para cobrir isso ainda.
ghci> zipWith3 (\x y z -> x + y + z) [1,2,3] [4,5,2,2] [2,2,3]
[7,9,8]
ghci> zip4 [2,3,3] [2,2,2] [5,5,3] [2,2,2]
[(2,2,5,2),(3,2,5,2),(3,2,3,2)]
Assim como no zipping normal, as listas mais longas que a lista mais curta que está sendo compactada são cortadas no tamanho.
lines é uma função útil ao lidar
com arquivos ou entrada de algum lugar. Pega uma string e retorna cada
linha dessa string como elemento separado de uma lista.
ghci> lines "first line\nsecond line\nthird line"
["first line","second line","third line"]
'\n' é o caractere para uma nova linha unix. As barras
invertidas têm significado especial em strings e caracteres de
Haskell.
unlines é a função inversa de
lines. Pega uma lista de strings e as une usando um
'\n'.
ghci> unlines ["first line", "second line", "third line"]
"first line\nsecond line\nthird line\n"
words e unwords são para dividir uma linha de
texto em palavras ou unir uma lista de palavras em um texto. Muito
útil.
ghci> words "hey these are the words in this sentence"
["hey","these","are","the","words","in","this","sentence"]
ghci> words "hey these are the words in this\nsentence"
["hey","these","are","the","words","in","this","sentence"]
ghci> unwords ["hey","there","mate"]
"hey there mate"
Já mencionamos nub. Pega uma
lista e elimina os elementos duplicados, retornando uma lista cujo
elemento é um floco de neve único! A função tem um nome meio estranho.
Acontece que “nub” significa um pequeno pedaço ou parte essencial de
algo. Na minha opinião, eles deveriam usar palavras reais para nomes de
funções em vez de palavras de pessoas velhas.
ghci> nub [1,2,3,4,3,2,1,2,3,4,3,2,1]
[1,2,3,4]
ghci> nub "Lots of words and stuff"
"Lots fwrdanu"
delete pega um elemento e uma
lista e exclui a primeira ocorrência desse elemento na lista.
ghci> delete 'h' "hey there ghang!"
"ey there ghang!"
ghci> delete 'h' . delete 'h' $ "hey there ghang!"
"ey tere ghang!"
ghci> delete 'h' . delete 'h' . delete 'h' $ "hey there ghang!"
"ey tere gang!"
\\ é a função de diferença de
lista. Ele age como uma diferença de conjunto, basicamente. Para cada
elemento na lista do lado direito, ele remove um elemento correspondente
na esquerda.
ghci> [1..10] \\ [2,5,9]
[1,3,4,6,7,8,10]
ghci> "Im a big baby" \\ "big"
"Im a baby"
Fazer [1..10] \\ [2,5,9] é como fazer
delete 2 . delete 5 . delete 9 $ [1..10].
union também atua como uma função
em conjuntos. Ele retorna a união de duas listas. Ele praticamente
repassa todos os elementos da segunda lista e o acrescenta ao primeiro
se ainda não estiver dentro. Cuidado, porém, as duplicatas são removidas
da segunda lista!
ghci> "hey man" `union` "man what's up"
"hey manwt'sup"
ghci> [1..7] `union` [5..10]
[1,2,3,4,5,6,7,8,9,10]
intersect funciona como
interseção de conjuntos. Ele retorna apenas os elementos encontrados nas
duas listas.
ghci> [1..7] `intersect` [5..10]
[5,6,7]
insert pega um elemento e uma
lista de elementos que podem ser classificados e o insere na última
posição, onde ainda é menor ou igual ao próximo elemento. Em outras
palavras, insert começará no início da lista e continuará
até encontrar um elemento igual ou maior que o elemento que estamos
inserindo e o inserirá logo antes do elemento.
ghci> insert 4 [3,5,1,2,8,2]
[3,4,5,1,2,8,2]
ghci> insert 4 [1,3,4,4,1]
[1,3,4,4,4,1]
O 4 é inserido logo após o 3 e antes do
5 no primeiro exemplo e entre 3 e
4 no segundo exemplo.
Se usarmos insert para inserir em uma lista
classificada, a lista resultante será mantida classificada.
ghci> insert 4 [1,2,3,5,6,7]
[1,2,3,4,5,6,7]
ghci> insert 'g' $ ['a'..'f'] ++ ['h'..'z']
"abcdefghijklmnopqrstuvwxyz"
ghci> insert 3 [1,2,4,3,2,1]
[1,2,3,4,3,2,1]
O que length, take, drop,
splitAt, !! e replicate têm em
comum é que eles tomam um Int como um de seus parâmetros
(ou retornam um Int), embora pudessem ser mais genéricos e
utilizáveis se apenas pegassem qualquer tipo que faça parte das
typeclasses Integral ou Num (dependendo das
funções). Eles fazem isso por razões históricas. No entanto, corrigir
isso provavelmente quebraria muito código existente. É por isso que
Data.List tem seus equivalentes mais genéricos, chamado
genericLength, genericTake, genericDrop, genericSplitAt, genericIndex e genericReplicate. Por exemplo,
length tem uma assinatura de tipo de
length :: [a] -> Int. Se tentarmos obter a média de uma
lista de números fazendo
let xs = [1..6] in sum xs / length xs, recebemos um erro de
tipo, porque você não pode usar / com um Int.
genericLength, por outro lado, tem uma assinatura de tipo
de genericLength :: (Num a) => [b] -> a. Como um
Num pode agir como um número de ponto flutuante, obter a
média fazendo let xs = [1..6] in sum xs / genericLength xs
funciona muito bem.
As funções nub, delete, union,
intersect e group têm todas as suas
contrapartes mais gerais chamadas nubBy, deleteBy, unionBy, intersectBy e groupBy. A diferença entre eles é que o
primeiro conjunto de funções usa == para testar a
igualdade, enquanto as By também recebem uma função de
igualdade e depois as comparam usando essa função de igualdade.
group é o mesmo que groupBy (==).
Por exemplo, digamos que temos uma lista que descreve o valor de uma
função a cada segundo. Queremos segmentá-la em sublistas com base em
quando o valor estava abaixo de zero e quando subiu. Se fizéssemos
apenas um group normal, ele apenas agruparia os valores
adjacentes iguais. Mas o que queremos é agrupá-los se eles são negativos
ou não. É aí que entra o groupBy! A função de igualdade
fornecida às funções By deve receber dois elementos do mesmo
tipo e retornar True se os considerar iguais pelos seus
padrões.
ghci> let values = [-4.3, -2.4, -1.2, 0.4, 2.3, 5.9, 10.5, 29.1, 5.3, -2.4, -14.5, 2.9, 2.3]
ghci> groupBy (\x y -> (x > 0) == (y > 0)) values
[[-4.3,-2.4,-1.2],[0.4,2.3,5.9,10.5,29.1,5.3],[-2.4,-14.5],[2.9,2.3]]
A partir disso, vemos claramente quais seções são positivas e quais
são negativas. A função de igualdade fornecida pega dois elementos e
retorna True apenas se ambos forem negativos ou se ambos
forem positivos. Essa função de igualdade também pode ser escrita como
\x y -> (x > 0) && (y > 0) || (x <= 0) && (y <= 0),
embora eu ache que a primeira maneira é mais legível. Uma maneira ainda
mais clara de escrever funções de igualdade para as funções By
é se você importar a função on de
Data.Function. on é definido assim:
on :: (b -> b -> c) -> (a -> b) -> a -> a -> c
f `on` g = \x y -> f (g x) (g y)
Portanto, fazer (==)on(> 0) retorna uma
função de igualdade semelhante a
\x y -> (x > 0) == (y > 0). on é
usado muito com as funções By porque com ela, podemos
fazer:
ghci> groupBy ((==) `on` (> 0)) values
[[-4.3,-2.4,-1.2],[0.4,2.3,5.9,10.5,29.1,5.3],[-2.4,-14.5],[2.9,2.3]]
Muito legível de fato! Você pode ler em voz alta: agrupe isso por igualdade se os elementos forem maiores que zero.
Da mesma forma, sort, insert,
maximum e minimum também têm seus equivalentes
mais gerais. Funções como groupBy pegam uma função que
determina quando dois elementos são iguais. sortBy, insertBy, maximumBy e minimumBy pegam uma função que determina
se um elemento é maior, menor ou igual ao outro. A assinatura de tipo de
sortBy é
sortBy :: (a -> a -> Ordering) -> [a] -> [a].
Se você se lembra de antes, o tipo Ordering pode ter um
valor de LT, EQ ou GT.
sort é o equivalente a sortBy compare, porque
compare apenas pega dois elementos cujo tipo está na typeclass
Ord e retorna seu relacionamento de pedidos.
Listas podem ser comparadas, mas quando são, são comparadas
lexicograficamente. E se tivermos uma lista de listas e queremos
classificá-la não com base no conteúdo das listas internas, mas em seus
comprimentos? Bem, como você provavelmente adivinhou, usaremos a função
sortBy.
ghci> let xs = [[5,4,5,4,4],[1,2,3],[3,5,4,3],[],[2],[2,2]]
ghci> sortBy (compare `on` length) xs
[[],[2],[2,2],[1,2,3],[3,5,4,3],[5,4,5,4,4]]
Incrível! compareonlength … cara, isso lê
quase como inglês real! Se você não tem certeza de como exatamente o
on funciona aqui, compareonlength
é o equivalente a
\x y -> length xcomparelength y. Quando
você está lidando com funções By que assumem uma função de
igualdade, geralmente faz (==)onsomething e
quando está lidando com funções By que executam uma função de
ordenação, geralmente faz
compareonsomething.
Data.Char

O módulo Data.Char faz o que o nome sugere. Ele exporta
funções que lidam com caracteres. Também é útil ao filtrar e mapear
strings, porque elas são apenas listas de caracteres.
Data.Char exporta um monte de predicados sobre
caracteres. Ou seja, funções que pegam um caractere e nos dizem se
alguma suposição sobre ele é verdadeira ou falsa. Aqui está o que eles
são:
isControl verifica se um
caractere é um caractere de controle.
isSpace verifica se um caractere
é um caractere de espaço em branco. Isso inclui espaços, caracteres de
tabulação, novas linhas, etc.
isLower verifica se um caractere
é minúsculo.
isUpper verifica se um caractere
é maiúsculo.
isAlpha verifica se um caractere
é uma letra.
isAlphaNum verifica se um
caractere é uma letra ou um número.
isPrint verifica se um caractere
é imprimível. Caracteres de controle, por exemplo, não são
imprimíveis.
isDigit verifica se um caractere
é um dígito.
isOctDigit verifica se um
caractere é um dígito octal.
isHexDigit verifica se um
caractere é um dígito hexadecimal.
isLetter verifica se um caractere
é uma letra.
isMark verifica se há caracteres
de marca Unicode. Esses são caracteres que combinam com as letras
anteriores para formar letras com sotaques. Use isso se você for
francês.
isNumber verifica se um caractere
é numérico.
isPunctuation verifica se um
caractere é pontuação.
isSymbol verifica se um caractere
é um símbolo matemático ou de moeda extravagante.
isSeparator verifica se há
espaços e separadores Unicode.
isAscii verifica se um caractere
cai nos primeiros 128 caracteres do conjunto de caracteres Unicode.
isLatin1 verifica se um caractere
cai nos primeiros 256 caracteres do Unicode.
isAsciiUpper verifica se um
caractere é ASCII e maiúsculo.
isAsciiLower verifica se um
caractere é ASCII e minúsculo.
Todos esses predicados têm uma assinatura de tipo de
Char -> Bool. Na maioria das vezes, você usará isso para
filtrar strings ou algo assim. Por exemplo, digamos que estamos fazendo
um programa que pega um nome de usuário que consiste apenas em
caracteres alfanuméricos. Podemos usar a função Data.List
all em combinação com os predicados Data.Char
para determinar se o nome de usuário está bem.
ghci> all isAlphaNum "bobby283"
True
ghci> all isAlphaNum "eddy the fish!"
False
Kewl. Caso você não se lembre, all pega um predicado e
uma lista e retorna True apenas se esse predicado for
válido para cada elemento da lista.
Também podemos usar isSpace para simular a função
Data.List words.
ghci> words "hey folks its me"
["hey","folks","its","me"]
ghci> groupBy ((==) `on` isSpace) "hey folks its me"
["hey"," ","folks"," ","its"," ","me"]
ghci>
Hmmm, bem, isso faz o que words faz, mas ficamos com
elementos de apenas espaços. Hmm, o que faremos? Eu sei, vamos filtrar
esse otário.
ghci> filter (not . any isSpace) . groupBy ((==) `on` isSpace) $ "hey folks its me"
["hey","folks","its","me"]
Ah.
O Data.Char também exporta um tipo de dados que é meio
que o Ordering. O tipo Ordering pode ter um
valor de LT, EQ ou GT. É uma
espécie de enumeração. Ele descreve alguns resultados possíveis que
podem surgir da comparação de dois elementos. O tipo
GeneralCategory também é uma enumeração. Ele nos apresenta
algumas categorias possíveis em que um caractere pode cair. A principal
função para obter a categoria geral de um caractere é
generalCategory. Tem um tipo de
generalCategory :: Char -> GeneralCategory. Existem
cerca de 31 categorias, então não vamos listá-las todas aqui, mas vamos
brincar com a função.
ghci> generalCategory ' '
Space
ghci> generalCategory 'A'
UppercaseLetter
ghci> generalCategory 'a'
LowercaseLetter
ghci> generalCategory '.'
OtherPunctuation
ghci> generalCategory '9'
DecimalNumber
ghci> map generalCategory " \t\nA9?|"
[Space,Control,Control,UppercaseLetter,DecimalNumber,OtherPunctuation,MathSymbol]
Como o tipo GeneralCategory faz parte da typeclass
Eq, também podemos testar coisas como
generalCategory c == Space.
toUpper converte um caractere em
maiúsculas. Espaços, números e afins permanecem inalterados.
toLower converte um caractere em
minúsculas.
toTitle converte um caractere
para title-case. Para a maioria dos caracteres, title-case é o mesmo que
maiúsculas.
digitToInt converte um caractere
em um Int. Para ter sucesso, o caractere deve estar nos
intervalos '0'..'9', 'a'..'f' ou
'A'..'F'.
ghci> map digitToInt "34538"
[3,4,5,3,8]
ghci> map digitToInt "FF85AB"
[15,15,8,5,10,11]
intToDigit é a função inversa de
digitToInt. Pega um Int na faixa de
0..15 e o converte em um caractere minúsculo.
ghci> intToDigit 15
'f'
ghci> intToDigit 5
'5'
As funções ord e chr
convertem caracteres em seus números correspondentes e vice-versa:
ghci> ord 'a'
97
ghci> chr 97
'a'
ghci> map ord "abcdefgh"
[97,98,99,100,101,102,103,104]
A diferença entre os valores ord de dois caracteres é
igual à distância entre eles na tabela Unicode.
A cifra de César é um método primitivo de codificar mensagens, deslocando cada caractere nelas por um número fixo de posições no alfabeto. Podemos criar facilmente uma espécie de cifra de César, só que não nos restringiremos ao alfabeto.
encode :: Int -> String -> String
encode shift msg =
let ords = map ord msg
shifted = map (+ shift) ords
in map chr shifted
Aqui, primeiro convertemos a string em uma lista de números. Em
seguida, adicionamos o valor de deslocamento a cada número antes de
converter a lista de números de volta aos caracteres. Se você é um
cowboy de composição, pode escrever o corpo desta função como
map (chr . (+ shift) . ord) msg. Vamos tentar codificar
algumas mensagens.
ghci> encode 3 "Heeeeey"
"Khhhhh|"
ghci> encode 4 "Heeeeey"
"Liiiii}"
ghci> encode 1 "abcd"
"bcde"
ghci> encode 5 "Marry Christmas! Ho ho ho!"
"Rfww~%Hmwnxyrfx&%Mt%mt%mt&"
Isso está codificado bem. A decodificação de uma mensagem é basicamente apenas deslocá-la de volta pelo número de lugares que foi deslocado em primeiro lugar.
decode :: Int -> String -> String
decode shift msg = encode (negate shift) msg
ghci> encode 3 "Im a little teapot"
"Lp#d#olwwoh#whdsrw"
ghci> decode 3 "Lp#d#olwwoh#whdsrw"
"Im a little teapot"
ghci> decode 5 . encode 5 $ "This is a sentence"
"This is a sentence"
Data.Map
Listas de associação (também chamadas de dicionários) são listas usadas para armazenar pares de chave-valor onde a ordem não importa. Por exemplo, podemos usar uma lista de associação para armazenar números de telefone, onde os números de telefone seriam os valores e os nomes das pessoas seriam as chaves. Não nos importamos em que ordem eles são armazenados, apenas queremos obter o número de telefone certo para a pessoa certa.
A maneira mais óbvia de representar listas de associação em Haskell seria ter uma lista de pares. O primeiro componente no par seria a chave, o segundo componente o valor. Aqui está um exemplo de uma lista de associação com números de telefone:
phoneBook =
[("amelia","555-2938")
,("freya","452-2928")
,("isabella","493-2928")
,("neil","205-2928")
,("roald","939-8282")
,("tenzing","853-2492")
]
Apesar dessa indentação aparentemente estranha, essa é apenas uma lista de pares de strings. A tarefa mais comum ao lidar com listas de associação é procurar algum valor por chave. Vamos fazer uma função que procure algum valor dada uma chave.
findKey :: (Eq k) => k -> [(k,v)] -> v
findKey key xs = snd . head . filter (\(k,v) -> key == k) $ xs
Bem simples. A função que pega uma chave e uma lista, filtra a lista
para que apenas as chaves correspondentes permaneçam, pega o primeiro
par chave-valor que corresponde e retorna o valor. Mas o que acontece se
a chave que estamos procurando não estiver na lista de associação? Hmm.
Aqui, se uma chave não estiver na lista de associação, acabaremos
tentando obter a cabeça de uma lista vazia, o que gera um erro de tempo
de execução. No entanto, devemos evitar tornar nossos programas tão
fáceis de travar; portanto, vamos usar o tipo de dados
Maybe. Se não encontrarmos a chave, retornaremos um
Nothing. Se o encontrarmos, retornaremos
Just something, onde algo é o valor correspondente a essa
chave.
findKey :: (Eq k) => k -> [(k,v)] -> Maybe v
findKey key [] = Nothing
findKey key ((k,v):xs) = if key == k
then Just v
else findKey key xs
Olhe para a declaração de tipo. Ele pega uma chave que pode ser equiparada, uma lista de associação e talvez produz um valor. Parece certo.
Esta é uma função recursiva de livro didático que opera em uma lista. Caso de borda, dividindo uma lista em uma cabeça e uma cauda, chamadas recursivas, estão todas lá. Esse é o padrão clássico de dobra, então vamos ver como isso seria implementado como uma dobra.
findKey :: (Eq k) => k -> [(k,v)] -> Maybe v
findKey key = foldr (\(k,v) acc -> if key == k then Just v else acc) Nothing
Nota: Geralmente, é melhor usar dobras para esse
padrão de recursão de lista padrão, em vez de escrever explicitamente a
recursão, porque são mais fáceis de ler e identificar. Todo mundo sabe
que é uma dobra quando vê a chamada foldr, mas é preciso
pensar um pouco mais para ler a recursão explícita.
ghci> findKey "tenzing" phoneBook
Just "853-2492"
ghci> findKey "amelia" phoneBook
Just "555-2938"
ghci> findKey "christopher" phoneBook
Nothing

Funciona como um encanto! Se tivermos o número de telefone do amigo,
Just obtemos o número, caso contrário, obtemos
Nothing.
Acabamos de implementar a função lookup de
Data.List. Se quisermos encontrar o valor correspondente a
uma chave, temos que percorrer todos os elementos da lista até
encontrá-la. O módulo Data.Map oferece listas de associação
muito mais rápidas (porque são implementadas internamente com árvores) e
também fornece muitas funções utilitárias. A partir de agora, diremos
que estamos trabalhando com mapas (maps) em vez de listas de
associação.
Como o Data.Map exporta funções que conflitam com as do
Prelude e Data.List, faremos uma importação
qualificada.
import qualified Data.Map as Map
Coloque esta declaração de importação em um script e, em seguida, carregue o script via GHCI.
Vamos em frente e ver o que Data.Map tem reservado para
nós! Aqui está o resumo básico de suas funções.
A função fromList pega uma lista
de associação (na forma de uma lista) e retorna um mapa com as mesmas
associações.
ghci> Map.fromList [("amelia","555-2938"),("freya","452-2928"),("neil","205-2928")]
fromList [("amelia","555-2938"),("freya","452-2928"),("neil","205-2928")]
ghci> Map.fromList [(1,2),(3,4),(3,2),(5,5)]
fromList [(1,2),(3,2),(5,5)]
Se houver chaves duplicadas na lista de associação original, as
duplicatas serão descartadas. Esta é a assinatura de tipo de
fromList
Map.fromList :: (Ord k) => [(k, v)] -> Map.Map k v
Ele diz que pega uma lista de pares do tipo k e
v e retorna um mapa que mapeia de chaves do tipo
k para o tipo v. Observe que, quando estávamos
fazendo listas de associação com listas normais, as chaves só precisavam
ser equiparáveis (seu tipo pertencente à typeclass Eq), mas
agora elas precisam ser ordenáveis. Essa é uma restrição essencial no
módulo Data.Map. Ele precisa que as chaves sejam ordenáveis
para que possa organizá-las em uma árvore.
Você deve sempre usar Data.Map para associações de
valores-chave, a menos que tenha chaves que não fazem parte da typeclass
Ord.
empty representa um mapa vazio.
Não aceita argumentos, apenas retorna um mapa vazio.
ghci> Map.empty
fromList []
insert pega uma chave, um valor e
um mapa e retorna um novo mapa que é exatamente como o antigo, apenas
com a chave e o valor inseridos.
ghci> Map.empty
fromList []
ghci> Map.insert 3 100 Map.empty
fromList [(3,100)]
ghci> Map.insert 5 600 (Map.insert 4 200 ( Map.insert 3 100 Map.empty))
fromList [(3,100),(4,200),(5,600)]
ghci> Map.insert 5 600 . Map.insert 4 200 . Map.insert 3 100 $ Map.empty
fromList [(3,100),(4,200),(5,600)]
Podemos implementar nosso próprio fromList usando o mapa
vazio, insert e uma dobra. Assista:
fromList' :: (Ord k) => [(k,v)] -> Map.Map k v
fromList' = foldr (\(k,v) acc -> Map.insert k v acc) Map.empty
É uma dobra bastante direta. Começamos com um mapa vazio e o dobramos da direita, inserindo os pares de valores-chave no acumulador à medida que avançamos.
null verifica se um mapa está
vazio.
ghci> Map.null Map.empty
True
ghci> Map.null $ Map.fromList [(2,3),(5,5)]
False
size relata o tamanho de um
mapa.
ghci> Map.size Map.empty
0
ghci> Map.size $ Map.fromList [(2,4),(3,3),(4,2),(5,4),(6,4)]
5
singleton pega uma chave e um
valor e cria um mapa que possui exatamente um mapeamento.
ghci> Map.singleton 3 9
fromList [(3,9)]
ghci> Map.insert 5 9 $ Map.singleton 3 9
fromList [(3,9),(5,9)]
lookup funciona como o
lookup de Data.List, apenas opera em mapas.
Ele retorna Just something se encontrar algo para a chave e
Nothing se não encontrar.
member é um predicado que pega
uma chave e um mapa e relata se a chave está no mapa ou não.
ghci> Map.member 3 $ Map.fromList [(3,6),(4,3),(6,9)]
True
ghci> Map.member 3 $ Map.fromList [(2,5),(4,5)]
False
map e filter funcionam muito como seus
equivalentes de lista.
ghci> Map.map (*100) $ Map.fromList [(1,1),(2,4),(3,9)]
fromList [(1,100),(2,400),(3,900)]
ghci> Map.filter isUpper $ Map.fromList [(1,'a'),(2,'A'),(3,'b'),(4,'B')]
fromList [(2,'A'),(4,'B')]
toList é o inverso de
fromList.
ghci> Map.toList . Map.insert 9 2 $ Map.singleton 4 3
[(4,3),(9,2)]
keys e elems retornam listas de chaves e valores,
respectivamente. keys é o equivalente a
map fst . Map.toList e elems é o equivalente a
map snd . Map.toList.
fromListWith é uma pequena função
legal. Ele age como fromList, mas não descarta chaves
duplicadas, mas usa uma função fornecida a ele para decidir o que fazer
com elas. Digamos que um amigo possa ter vários números e tenhamos uma
lista de associação configurada assim.
phoneBook =
[("amelia","555-2938")
,("amelia","342-2492")
,("freya","452-2928")
,("isabella","493-2928")
,("isabella","943-2929")
,("isabella","827-9162")
,("neil","205-2928")
,("roald","939-8282")
,("tenzing","853-2492")
,("tenzing","555-2111")
]
Agora, se apenas usarmos fromList para colocar isso em
um mapa, perderemos alguns números! Então, aqui está o que faremos:
phoneBookToMap :: (Ord k) => [(k, String)] -> Map.Map k String
phoneBookToMap xs = Map.fromListWith (\number1 number2 -> number1 ++ ", " ++ number2) xs
ghci> Map.lookup "isabella" $ phoneBookToMap phoneBook
"827-9162, 943-2929, 493-2928"
ghci> Map.lookup "roald" $ phoneBookToMap phoneBook
"939-8282"
ghci> Map.lookup "amelia" $ phoneBookToMap phoneBook
"342-2492, 555-2938"
Se uma chave duplicada for encontrada, a função que passamos será
usada para combinar os valores dessas chaves em algum outro valor.
Também poderíamos fazer com que todos os valores na lista de associação
fossem listas singleton e, em seguida, podemos usar ++ para
combinar os números.
phoneBookToMap :: (Ord k) => [(k, a)] -> Map.Map k [a]
phoneBookToMap xs = Map.fromListWith (++) $ map (\(k,v) -> (k,[v])) xs
ghci> Map.lookup "isabella" $ phoneBookToMap phoneBook
["827-9162","943-2929","493-2928"]
Muito legal! Outro caso de uso é se estamos criando um mapa a partir de uma lista de associação de números e, quando uma chave duplicada é encontrada, queremos que o maior valor da chave seja mantido.
ghci> Map.fromListWith max [(2,3),(2,5),(2,100),(3,29),(3,22),(3,11),(4,22),(4,15)]
fromList [(2,100),(3,29),(4,22)]
Ou poderíamos optar por somar valores nas mesmas chaves.
ghci> Map.fromListWith (+) [(2,3),(2,5),(2,100),(3,29),(3,22),(3,11),(4,22),(4,15)]
fromList [(2,108),(3,62),(4,37)]
insertWith está para
insert assim como fromListWith está para
fromList. Ele insere um par de chave-valor em um mapa, mas
se esse mapa já contiver a chave, ele usa a função passada para
determinar o que fazer.
ghci> Map.insertWith (+) 3 100 $ Map.fromList [(3,4),(5,103),(6,339)]
fromList [(3,104),(5,103),(6,339)]
Essas foram apenas algumas funções do Data.Map. Você
pode ver uma lista completa de funções na documentação.
Data.Set

O módulo Data.Set nos oferece, bem, conjuntos. Como
conjuntos da matemática. Conjuntos são como um cruzamento entre listas e
mapas. Todos os elementos de um conjunto são únicos. E como eles são
implementados internamente com árvores (como mapas em
Data.Map), eles são ordenados. Verificar a associação,
inserir, excluir etc. é muito mais rápido do que fazer a mesma coisa com
as listas. A operação mais comum ao lidar com conjuntos é inserir em um
conjunto, verificar a associação e converter um conjunto em uma
lista.
Como os nomes em Data.Set entram em conflito com muitos
nomes de Prelude e Data.List, fazemos uma
importação qualificada.
Coloque esta declaração de importação em um script:
import qualified Data.Set as Set
E então carregue o script via GHCI.
Digamos que temos dois pedaços de texto. Queremos descobrir quais caracteres foram usados em ambos.
text1 = "I just had an anime dream. Anime... Reality... Are they so different?"
text2 = "The old man left his garbage can out and now his trash is all over my lawn!"
A função fromList funciona muito
como você esperaria. Pega uma lista e a converte em um conjunto.
ghci> let set1 = Set.fromList text1
ghci> let set2 = Set.fromList text2
ghci> set1
fromList " .?AIRadefhijlmnorstuy"
ghci> set2
fromList " !Tabcdefghilmnorstuvwy"
Como você pode ver, os itens são ordenados e cada elemento é único.
Agora vamos usar a função intersection para ver quais elementos
ambos compartilham.
ghci> Set.intersection set1 set2
fromList " adefhilmnorstuy"
Podemos usar a função difference
para ver quais letras estão no primeiro conjunto, mas não estão no
segundo e vice-versa.
ghci> Set.difference set1 set2
fromList ".?AIRj"
ghci> Set.difference set2 set1
fromList "!Tbcgvw"
Ou podemos ver todas as letras únicas usadas nas duas frases usando
union.
ghci> Set.union set1 set2
fromList " !.?AIRTabcdefghijlmnorstuvwy"
As funções null, size, member, empty, singleton, insert e delete funcionam todas como você
esperaria.
ghci> Set.null Set.empty
True
ghci> Set.null $ Set.fromList [3,4,5,5,4,3]
False
ghci> Set.size $ Set.fromList [3,4,5,3,4,5]
3
ghci> Set.singleton 9
fromList [9]
ghci> Set.insert 4 $ Set.fromList [9,3,8,1]
fromList [1,3,4,8,9]
ghci> Set.insert 8 $ Set.fromList [5..10]
fromList [5,6,7,8,9,10]
ghci> Set.delete 4 $ Set.fromList [3,4,5,4,3,4,5]
fromList [3,5]
Também podemos verificar subconjuntos ou subconjunto próprio. O conjunto A é um subconjunto do conjunto B se B contiver todos os elementos que A possui. O conjunto A é um subconjunto próprio do conjunto B se B contiver todos os elementos que A possui, mas tiver mais elementos.
ghci> Set.fromList [2,3,4] `Set.isSubsetOf` Set.fromList [1,2,3,4,5]
True
ghci> Set.fromList [1,2,3,4,5] `Set.isSubsetOf` Set.fromList [1,2,3,4,5]
True
ghci> Set.fromList [1,2,3,4,5] `Set.isProperSubsetOf` Set.fromList [1,2,3,4,5]
False
ghci> Set.fromList [2,3,4,8] `Set.isSubsetOf` Set.fromList [1,2,3,4,5]
False
Também podemos mapear map sobre
conjuntos e filtrar filter eles.
ghci> Set.filter odd $ Set.fromList [3,4,5,6,7,2,3,4]
fromList [3,5,7]
ghci> Set.map (+1) $ Set.fromList [3,4,5,6,7,2,3,4]
fromList [3,4,5,6,7,8]
Os conjuntos são frequentemente usados para eliminar uma lista de
duplicatas de uma lista, transformando-a primeiro em um conjunto com
fromList e depois convertendo-o de volta para uma lista com
toList. A função
Data.List nub já faz isso, mas eliminar
duplicatas para listas grandes é muito mais rápido se você as colocar em
um conjunto e depois convertê-las novamente em uma lista do que usar o
nub. Mas usar nub exige apenas que o tipo dos
elementos da lista faça parte da typeclass Eq, ao passo
que, se você deseja colocar elementos em um conjunto, o tipo da lista
deve estar em Ord.
ghci> let setNub xs = Set.toList $ Set.fromList xs
ghci> setNub "HEY WHATS CRACKALACKIN"
" ACEHIKLNRSTWY"
ghci> nub "HEY WHATS CRACKALACKIN"
"HEY WATSCRKLIN"
setNub geralmente é mais rápido que nub em
grandes listas, mas como você pode ver, nub preserva a
ordem dos elementos da lista, enquanto setNub não.
Fazendo nossos próprios módulos (Making our own modules)

Vimos alguns módulos legais até agora, mas como fazemos nosso próprio módulo? Quase todas as linguagens de programação permitem dividir seu código em vários arquivos e o Haskell não é diferente. Ao fazer programas, é uma boa prática pegar funções e tipos que trabalham para um objetivo semelhante e colocá-los em um módulo. Dessa forma, você pode reutilizar facilmente essas funções em outros programas apenas importando seu módulo.
Vamos ver como podemos criar nossos próprios módulos criando um
pequeno módulo que fornece algumas funções para calcular o volume e a
área de alguns objetos geométricos. Começaremos criando um arquivo
chamado Geometry.hs.
Dizemos que um módulo exporta funções. O que isso significa é que, quando importo um módulo, posso usar as funções que ele exporta. Ele pode definir funções que suas funções chamam internamente, mas só podemos ver e usar as que ele exporta.
No início de um módulo, especificamos o nome do módulo. Se tivermos
um arquivo chamado Geometry.hs, devemos nomear nosso módulo
Geometry. Em seguida, especificamos as funções que ele
exporta e, depois disso, podemos começar a escrever as funções. Então
começaremos com isso.
module Geometry
( sphereVolume
, sphereArea
, cubeVolume
, cubeArea
, cuboidArea
, cuboidVolume
) where
Como você pode ver, faremos áreas e volumes para esferas, cubos e cubóides. Vamos em frente e definir nossas funções então:
module Geometry
( sphereVolume
, sphereArea
, cubeVolume
, cubeArea
, cuboidArea
, cuboidVolume
) where
sphereVolume :: Float -> Float
sphereVolume radius = (4.0 / 3.0) * pi * (radius ^ 3)
sphereArea :: Float -> Float
sphereArea radius = 4 * pi * (radius ^ 2)
cubeVolume :: Float -> Float
cubeVolume side = cuboidVolume side side side
cubeArea :: Float -> Float
cubeArea side = cuboidArea side side side
cuboidVolume :: Float -> Float -> Float -> Float
cuboidVolume a b c = rectangleArea a b * c
cuboidArea :: Float -> Float -> Float -> Float
cuboidArea a b c = rectangleArea a b * 2 + rectangleArea a c * 2 + rectangleArea c b * 2
rectangleArea :: Float -> Float -> Float
rectangleArea a b = a * b
Geometria bastante padrão aqui. Há algumas coisas a serem observadas.
Como um cubo é apenas um caso especial de cubóide, definimos sua área e
volume, tratando-o como um cubóide cujos lados são todos do mesmo
comprimento. Também definimos uma função auxiliar chamada
rectangleArea, que calcula a área de um retângulo com base
no comprimento de seus lados. É bastante trivial, porque é apenas
multiplicação. Observe que o usamos em nossas funções no módulo (a saber
cuboidArea e cuboidVolume), mas não o
exportamos! Como queremos que nosso módulo apresente apenas funções para
lidar com objetos tridimensionais, usamos rectangleArea,
mas não o exportamos.
Ao criar um módulo, geralmente exportamos apenas as funções que atuam
como uma espécie de interface para o nosso módulo, para que a
implementação seja ocultada. Se alguém estiver usando nosso módulo
Geometry, não precisará se preocupar com funções que não
exportamos. Podemos decidir mudar essas funções completamente ou
excluí-las em uma versão mais recente (poderíamos excluir
rectangleArea e apenas usar * em vez disso) e
ninguém se importará, porque não as exportávamos em primeiro lugar.
Para usar nosso módulo, basta fazer:
import Geometry
Geometry.hs deve estar na mesma pasta em que está o
programa que o importa.
Os módulos também podem ter uma estrutura hierárquica. Cada módulo
pode ter vários submódulos e eles podem ter seus próprios submódulos.
Vamos separar essas funções para que Geometry seja um
módulo que tenha três submódulos, um para cada tipo de objeto.
Primeiro, faremos uma pasta chamada Geometry. Cuidado
com o G maiúsculo. Nele, colocaremos três arquivos:
Sphere.hs, Cuboid.hs e Cube.hs.
Aqui está o que os arquivos conterão:
Sphere.hs
module Geometry.Sphere
( volume
, area
) where
volume :: Float -> Float
volume radius = (4.0 / 3.0) * pi * (radius ^ 3)
area :: Float -> Float
area radius = 4 * pi * (radius ^ 2)
Cuboid.hs
module Geometry.Cuboid
( volume
, area
) where
volume :: Float -> Float -> Float -> Float
volume a b c = rectangleArea a b * c
area :: Float -> Float -> Float -> Float
area a b c = rectangleArea a b * 2 + rectangleArea a c * 2 + rectangleArea c b * 2
rectangleArea :: Float -> Float -> Float
rectangleArea a b = a * b
Cube.hs
module Geometry.Cube
( volume
, area
) where
import qualified Geometry.Cuboid as Cuboid
volume :: Float -> Float
volume side = Cuboid.volume side side side
area :: Float -> Float
area side = Cuboid.area side side side
Tudo certo! Então, primeiro é Geometry.Sphere. Observe
como o colocamos em uma pasta chamada Geometry e depois
definimos o nome do módulo como Geometry.Sphere. Fizemos o
mesmo para o cubóide. Observe também como nos três submódulos definimos
funções com os mesmos nomes. Podemos fazer isso porque são módulos
separados. Queremos usar funções de Geometry.Cuboid em
Geometry.Cube, mas não podemos simplesmente fazer
import Geometry.Cuboid porque exporta funções com os mesmos
nomes que Geometry.Cube. É por isso que fazemos uma
importação qualificada e está tudo bem.
Então, agora, se estivermos em um arquivo que está no mesmo nível que
a pasta Geometry, podemos fazer, digamos:
import Geometry.Sphere
E então podemos chamar area e volume e eles
nos darão a área e o volume para uma esfera. E se quisermos fazer
malabarismos com dois ou mais desses módulos, temos que fazer
importações qualificadas porque eles exportam funções com os mesmos
nomes. Então, fazemos algo como:
import qualified Geometry.Sphere as Sphere
import qualified Geometry.Cuboid as Cuboid
import qualified Geometry.Cube as Cube
E então podemos chamar Sphere.area,
Sphere.volume, Cuboid.area, etc. e cada um
calculará a área ou o volume para seu objeto correspondente.
Na próxima vez que você se encontrar escrevendo um arquivo realmente grande e com muitas funções, tente ver quais funções atendem a algum objetivo comum e depois veja se você pode colocá-las em seu próprio módulo. Você poderá importar seu módulo na próxima vez em que estiver escrevendo um programa que requer a mesma funcionalidade.