Saltar para o conteúdo

Haskell/Módulos

Origem: Wikilivros, livros abertos por um mundo aberto.


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

Módulos são a principal forma de organizar código em Haskell. Nós os conhecemos quando declaramos import para inserir uma biblioteca de funções em nosso código. Além de nos permitir fazer melhor uso de bibliotecas, conhecer os módulos irá nos ajudar a construir nossos próprios programas e criar programas completos, os quais podem ser executados independente do GHCi (aliás, esse é o tópico do próximo capítulo, Programas completos).

Os Módulos do Haskell[1] são uma maneira útil de agrupar um conjunto de funções com funcionalidade relacionadas em um único pacote e gerenciar diferentes funções com mesmo nomes.

As declarações e importações de módulos devem ser escritas no início dos seus arquivos Haskell.

A definição básica de módulos é deste modelo:

module SeuModulo where

Note que:

  1. O nome do módulo inicia com letra maiúscula;
  2. Cada arquivo deve conter apenas um módulo.

O nome do arquivo é o nome do módulo mais a extensão .hs. Qualquer ponto '.' no nome do módulo é alterado por diretórios. [2] Então, o módulo SeuModulo estará no arquivo SeuModulo.hs, enquanto o módulo MeuDiretorio.MeuModulo estará no arquivo MeuDiretorio/MeuModulo. Como o nome do módulo inicia com uma letra maiúscula, o nome do arquivo também deve iniciar.

Os módulos podem importar funções de outros módulos. Isto é, entre a declaração do módulo e o resto do código você pode adicionar declarações import, desta forma:

import Data.Char (toLower, toUpper) -- importa apenas as funções toLower e toUpper do módulo Data.Char

import Data.List -- importa todas as funções exportadas do módulo Data.List
 
import MeuModulo -- importa todas as funções do módulo MeuModulo

datatypes importados são especificados pelos seus nomes, seguidos por uma lista de seus construtores[3] importados, essa lista deve estar entre parêntesis, veja:

import Data.Tree (Tree(Node)) -- importa apenas o datatype Tree e o construtor Node

E se importarmos alguns módulos com definição sobreposta? E se importarmos um módulo, mas quisermos sobrescrever algumas de suas funções? Há três caminhos para lidar com esses casos: Importação qualificada, escondendo definição e renomeando importações.

Importação qualificada

[editar | editar código-fonte]

Nos códigos abaixo, MeuModulo e MeuOutroModulo ambos possuem uma definição para remove_e, que remove todas as letras e da uma string. No entanto, MeuModulo remove apenas e minúsculos, e MeuOutroModulo remove ambos, minúsculos e maiúsculos. Nesse caso, o seguinte código é ambíguo:

import MeuModulo
import MeuOutroModulo

-- umaFuncao adiciona um 'c' no início da ''string'' e remove todos os 'e' 
umaFuncao :: String -> String
umaFuncao text = 'c' : remove_e text

Não fica claro o que remove_e significa. Para evitar isso, é usado a palavra-chave qualified:

import qualified MeuModulo
import qualified MeuOutroModulo

umaFuncao text = 'c' : MeuModulo.remove_e text -- remove 'e' minúsculos
outraFuncao text = 'c' : MeuOutroModulo.remove_e text -- remove todos os 'e'
umaFuncaoIncorreta text = 'c' : remove_e text -- Não funciona, pois não há uma função remove_e definida

No último trecho de código, não há uma função disponível com o nome remove_e. Quando fazemos importação qualificada todos os valores e funções importados incluem o nome do módulo como prefixo. Aliás, você também pode incluir o prefixo mesmo fazendo a importação comum (sem a palavra-chave qualified inclusa)[4].

Escondendo definição

[editar | editar código-fonte]

Agora, suponha que queremos importar ambos, MeuModulo e MeuOutroModulo, mas sabemos que queremos remover todos os e's, não apenas os minúsculos. Ficará muito tedioso adicionar MeuOutroModulo toda vez que chamar remove_e. Não podemos apenas excluir o remove_e do MeuModulo?

import MeuModulo hiding (remove_e)
import MeuOutroModulo

umaFuncao text = 'c' : remove_e text

Isso funciona por causa da palavra hiding. O que vier em seguida dessa palavra-chave não será importado. Esconde-se múltiplos itens listando-os entre parêntesis e separando-os por virgula.

import MeuModulo hiding (remove_e, remove_f)

Note que datatypes e tipos sinônimos não podem ser removidos. Eles são sempre importados. Se você tem um datatype definido em múltiplos arquivos, você deve usar nomes qualificados.


Renomeando importações

[editar | editar código-fonte]

Imagine que você tem um módulo com um nome longo, como este:

import qualified MeuModuloComUmNomeMuitoGrande

umaFuncao text = 'c' : MeuModuloComUmNomeMuitoGrande.remove_e text

Especialmente quando usamos importação qualificada, isto é irritante. Podemos melhorar isso usando a palavra-chave as:

import qualified MeuModuloComUmNomeMuitoGrande as Pequeno

umaFuncao text = 'c' : Pequeno.remove_e text

Isso nos permite usar Pequeno ao invés de usar MeuModuloComUmNomeMuitoGrande como prefixo para as funções importadas. Essa renomeação funciona para importações comuns e qualificadas.

Desde que não haja itens conflitantes, você pode importar múltiplos módulos e renomeá-los para o mesmo nome:

import MeuModulo as Meu
import MeuModuloComUmNomeMuitoGrande as Meu

Nesse caso ambas as funções em MeuModulo e em MeuModuloComUmNomeMuitoGrande podem ser prefixadas com Meu.

Combinando renomeação com importação limitadas

[editar | editar código-fonte]

Às vezes, é conveniente usar diretiva de importação duas vezes para o mesmo módulo. Um cenário típico é o seguinte:

import qualified Data.Set as Set
import Data.Set (Set, empty, insert)

Isso dá acesso a todos os módulos de Data.Set pelo apelido Set, e também as funções (empty, insert, e o construtor Set sem usar o prefixo Set.

No exemplo do início deste artigo, é falado "importa todas as funções exportadas do módulo".[5] Isto levanta uma questão, como decidimos quais funções podem ser exportadas e quais permanecem internas? Desta forma:

module MeuModulo (remove_e, add_two) where

add_one blah = blah + 1

remove_e text = filter (/= 'e') text

add_two blah = add_one . add_one $ blah

Nesse caso, somente remove_e e add_two são exportadas. Enquanto add_two é permitido usar add_one, funções em módulos que importarão MeuModulo não podem usar add_one diretamente, pois, não foi importada.

Exportar tipo de dados é semelhante as suas importação. Você digita o nome do tipo e uma lista de construtores entre parêntesis:

module MeuModulo2 (Arvore(Galho, Folha)) where

data Arvore a = Galho {esquerda, direita :: Arvore a} 
            | Folha a

Nesse caso, a declaração do módulo pode ser reescrita "MeuModulo2 (Arvore(..))", declarando que todos os construtores são exportados.

Manter uma lista de exportação é uma boa prática não somente porque reduz a poluição causada por a exportação de funções que não são utilizadas, como também habilita certas otimizações que em outros casos não são possíveis.

  1. Veja adocumentação original para mais detalhes.
  2. No haskell 98, a última versão padronizada antes do Haskell 2010, o sistema de módulos era bastante conservador, mas a prática comum recente consiste em empregar um sistema hierárquico de módulos, usando períodos para separar os espaços para nome.
  3. É falado sobre construtores de tipos e dados em Declaração de Tipos
  4. Há uma ambiguidade entre um nome qualificado (como, MeuModulo.remove_e) e o operador de composição (.). Escrever reverse.MeuModulo.remove_e provavelmente confunde seu compilador Haskell. Uma solução é: sempre use espaços entre este operador, por exemplo reverse . MeuModulo.remove_e
  5. Um módulo pode exportar funções que ele importa. Recursão mútua é possível, mas, precisa de uma atenção especial.