ReasonML: O básico sobre módulos

Módulos, Submódulos e suas interfaces

Esse artigo faz parte da série “O que é ReasonML?”.

Nesse artigo, iremos explorar como os módulos funcionam em ReasonML.

1. Instalando o repositório de exemplo

O repositório de exemplo para esse artigo está diposnível no GitHub: reasonml-demo-modules. Para instalar, faça o download e:

cd reasonml-demo-modules/
npm install

Isso é tudo que você precisa fazer, não há instalações globais necessárias. Se você quiser mais suporte para ReasonML, consulte “Começando com ReasonML”.

2. Seu primeiro programa ReasonML

Aqui é onde o seu primeiro programa ReasonML está localizado:

reasonml-demo-modules/
src/
HelloWorld.re

Em ReasonML, cada arquivo cujo nome tem a extensão .re é um módulo. Os nomes dos módulos começam com letras maiúsculas e são camel-cased. Nomes de arquivo definem os nomes de seus módulos, seguindo as mesmas regras.

Os programas são apenas módulos que você executa a partir de uma linha de comando.

HelloWorld.re, se parece com:

/* HelloWorld.re */let () = {
print_string("Hello world!");
print_newline()
};

Esse código pode parecer um pouco estranho, vamos passo a passo: estamos executando as duas linhas dentro das chaves e atribuindo o seu resultado para o padrão (). Ou seja, nenhuma nova variável é criada, mas o padrão garante que o resultado é (). O tipo de (), é unit, semelhante ao void em linguagens de estilo C.

Observe que não estamos definindo uma função, estamos imediatamente executando print_string() e print_newline().

Para compilar esse código, você tem duas opções (veja package.json para ver mais scripts a serem executados):

  • Compilar tudo, uma vez: npm run build
  • Observar todos os arquivos e compilar incrementalmente somente os arquivos que mudam: npm run watch

Portanto, nosso próximo passo é (executar em uma janela de terminal separada ou executar a última etapa em segundo plano):

cd reasonml-demo-modules/
npm run watch

Ao lado de HelloWorld.re, há agora um arquivo HelloWorld.bs.js. Você pode executar esse arquivo da seguinte maneira.

cd reasonml-demo-modules/
node src/HelloWorld.bs.js

2.1 Outra versão de HelloWorld.re

Como alternativa à nossa abordagem (que é uma convenção OCaml comum), poderíamos simplesmente colocar as duas linhas no âmbito global:

/* HelloWorld.re */print_string("Hello world!");
print_newline();

E poderíamos também, definir uma função main() para ser chamada:

/* HelloWorld.re */let main = () => {
print_string("Hello world!");
print_newline()
};
main();

3. Dois módulos simples

Vamos continuar com um módulo MathTools.re que será usado por outro módulo, Main.re:

reasonml-demo-modules/
src/
Main.re
MathTools.re

O módulo MathTools.re se parece com:

/* MathTools.re */let times = (x, y) => x * y;
let square = (x) => times(x, x);

E o módulo Main se parece com:

/* Main.re */let () = {
print_string("Result: ");
print_int(MathTools.square(3));
print_newline()
};

Como você pode ver, em ReasonML, você pode usar os módulos bastando mencionar os seus nomes. Eles são encontrados em qualquer lugar dentro do projeto atual.

3.1 Submódulos

Você também pode aninhar módulos. Isso também funciona:

/* Main.re */module MathTools = {
let times = (x, y) => x * y;
let square = (x) => times(x, x);
};
let () = {
print_string("Result: ");
print_int(MathTools.square(3));
print_newline()
};

Externamente, você pode acessar MathTools via Main.MathTools.

Vamos aninhar mais:

/* Main.re */module Math = {
module Tools = {
let times = (x, y) => x * y;
let square = (x) => times(x, x);
};
};
let () = {
print_string("Result: ");
print_int(Math.Tools.square(3));
print_newline()
};

4. Controlando como os valores são exportados de módulos

Por padrão, cada módulo, tipo e valor de um módulo são exportados. Se você deseja ocultar algumas dessas exportações, você deve usar interfaces. Além disso, interfaces oferecem suporte a tipos abstratos (cujos internos estão ocultos).

4.1 Arquivos como interfaces

Você pode controlar o quanto você quer exportar através das chamadas interfaces. Para um módulo definido por um arquivo foo.re, você coloca a interface em um arquivo foo.rei. Por exemplo:

/* MathTools.rei */let times: (int, int) => int;
let square: (int) => int;

Se, por exemplo, você omitir times no arquivo de interface, ele não será exportado.

A interface de um módulo também é chamada de sua assinatura, signature.

Se existir um arquivo de interface, então, comentários docblock devem ser colocados lá. Caso contrário, você colocá-los no arquivo .re.

Felizmente, não temos que escrever interfaces à mão, podemos gerá-los a partir de módulos. Como é descrito na documentação do BuckleScript. Para MathTools.rei, podemos fazer:

bsc -bs-re-out lib/bs/src/MathTools-ReasonmlDemoModules.cmi

4.2 Definindo interfaces para submódulos

Vamos supor, MathTools não reside em seu próprio arquivo, mas existe como um submódulo:

module MathTools = {
let times = (x, y) => x * y;
let square = (x) => times(x, x);
};

Como definimos uma interface para este módulo? Temos duas opções.

Primeiro, podemos definir e nomear uma interface via tipo de módulo:

module type MathToolsInterface = {
let times: (int, int) => int;
let square: (int) => int;
};

Essa interface se torna o tipo de módulo MathTools:

module MathTools: MathToolsInterface = {
···
};

Em segundo lugar, também podemos embutir a interface:

module MathTools: {
let times: (int, int) => int;
let square: (int) => int;
} = {
···
};

4.3 Tipos abstratos: ocultando partes internas

Você pode usar interfaces para ocultar os detalhes de tipos. Vamos começar com um módulo log.re que permite que você coloque strings “em” logs. Ele implementa logs via seqüências de caracteres e expõe completamente esse detalhe de implementação usando seqüências de caracteres diretamente:

/* Log.re */let make = () => "";
let logStr = (str: string, log: string) => log ++ str ++ "\n";
let print = (log: string) => print_string(log);

A partir deste código, não é claro que make() e logStr() realmente retornam logs.

É assim que você usa Log. Note como conveniente o operador pipe (|>) é neste caso:

/* LogMain.re */let () = Log.make()
|> Log.logStr("Hello")
|> Log.logStr("everyone")
|> Log.print;
/* Output:
Hello
everyone
*/

O primeiro passo para melhorar o log é introduzindo um tipo de logs. A convenção, emprestada de OCaml, é usar o nome t para o tipo principal suportado por um módulo. Por exemplo: Bytes.t

/* Log.re */type t = string; /* A */let make = (): t => "";
let logStr = (str: string, log: t): t => log ++ str ++ "\n";
let print = (log: t) => print_string(log);

Na linha A, nós definimos t para ser simplesmente um alias para Strings. Aliases são convenientes para que você possa começar simples e adicionar mais recursos mais tarde. No entanto, o alias nos força a anotar os resultados de make() e logStr() (que de outra forma teria o tipo de retorno String).

O arquivo de interface completo, seria como o seguinte:

/* Log.rei */type t = string; /* A */
let make: (unit) => t;
let logStr: (string, t) => t;
let print: (t) => unit;

Nós podemos substituir a linha A com o seguinte código e t se tornaria abstrato, seus detalhes são escondidos. Isso significa que podemos facilmente mudar nossa mente no futuro e, por exemplo, implementá-lo através de um array.

type t;

Convenientemente, nós não temos que mudar LogMain.re, ele irá funcionar, mesmo com o módulo novo.

5. Importando valores dos módulos

Há várias maneiras pelas quais você pode importar valores de módulos.

5.1 Importação via nomes qualificados

Já vimos que você pode importar automaticamente um valor exportado por um módulo se você qualificar o nome do valor com o nome do módulo. Por exemplo, no código a seguir, importamos make, logStr e print do módulo Log:

let () = Log.make()
|> Log.logStr("Hello")
|> Log.logStr("everyone")
|> Log.print;

5.2 Abrindo módulos globalmente

Você pode omitir o qualificador “Log.” se você abrir o Log “globalmente” (dentro do escopo do módulo atual):

open Log;let () = make()
|> logStr("Hello")
|> logStr("everyone")
|> print;

Para evitar confrontos de nomes, esta operação não é usada com muita frequência. A maioria dos módulos, como List, são usados por meio de qualificações: List.length(), List.map(), etc.

A abertura global também pode ser usada para optar por diferentes implementações para módulos padrão. Por exemplo, o módulo Foo pode ter um submódulo List. Em seguida, ao abrir Foo, ele irá substituir o módulo de List padrão.

5.3 Abrindo módulos localmente

Podemos minimizar o risco de confrontos de nomes, enquanto obtemos a conveniência de um módulo aberto, abrindo Log localmente. Fazemos isso prefixando uma expressão entre parênteses com Log. (i.e., estamos qualificando essa expressão). Por exemplo:

let () = Log.(
make()
|> logStr("Hello")
|> logStr("everyone")
|> print
);

Redefinindo operadores

Convenientemente, os operadores também são apenas funções em ReasonML. Que nos permite substituir temporariamente os operadores internos. Por exemplo, se você não gostar de ter que usar operadores com pontos para a matemática de ponto flutuante:

let dist = (x, y) =>
sqrt((x *. x) +. (y *. y));

Então nós podemos substituir os operadores int através de um módulo FloatOps:

module FloatOps = {
let (+) = (+.);
let (*) = (*.);
};
let dist = (x, y) =>
FloatOps.(
sqrt((x * x) + (y * y))
);

Se você realmente deve fazer isso no código de produção é discutível.

5.4 Includingo módulos

Outra maneira de importar um módulo é incluí-lo. Em seguida, todas as suas exportações são adicionadas às exportações do módulo atual. Isso é semelhante a herança entre classes na programação orientada a objeto.

No exemplo a seguir, o módulo LogWithDate é uma extensão do módulo Log. Ele tem a nova função logStrWithDate(), além de todas as funções de Log.

/* LogWithDateMain.re */module LogWithDate = {
include Log;
let logStrWithDate = (str: string, log: t) => {
let dateStr = Js.Date.toISOString(Js.Date.make());
logStr("[" ++ dateStr ++ "] " ++ str, log);
};
};
let () = LogWithDate.(
make()
|> logStrWithDate("Hello")
|> logStrWithDate("everyone")
|> print
);

Js.Date vem da biblioteca padrão do BuckleScript e não é explicado aqui.

Você pode incluir quantos módulos quiser, não apenas um.

5.5 Incluíndo interfaces

Interfaces são incluídas como a seguir: (InterfaceB estende InterfaceA):

module type InterfaceA = {
···
};
module type InterfaceB = {
include InterfaceA;
···
}

Da mesma forma que os módulos, você pode incluir mais de uma interface.

Vamos criar uma interface para o módulo LogWithDate. Infelizmente, não podemos incluir a interface do módulo Log por nome, porque ele não tem um. Podemos, no entanto, consultá-lo indiretamente, através do seu módulo (linha A):

module type LogWithDateInterface = {
include (module type of Log); /* A */
let logStrWithDate: (t, t) => t;
};
module LogWithDate: LogWithDateInterface = {
include Log;
···
};

5.6 Renomeando importações

Você não pode realmente renomear importações, mas você pode criar alias para elas.

É assim que você cria alias para módulos:

module L = List;

É assim que você cria alias para valores dentro de módulos:

let m = List.map;

6. Módulos com namespacing

Em grandes projetos, a maneira do ReasonML identificar módulos pode se tornar problemática. Como ele tem um único namespace de módulo global, pode facilmente haver conflitos de nomes. Digamos, dois módulos chamados Util em diretórios diferentes.

Uma técnica é criar um namespacing para os módulos. Por exemplo, o seguinte projeto:

proj/
foo/
NamespaceA.re
NamespaceA_Misc.re
NamespaceA_Util.re
bar/
baz/
NamespaceB.re
NamespaceB_Extra.re
NamespaceB_Tools.re
NamespaceB_Util.re

Há dois módulos Util neste projeto cujos nomes só são distintos porque eles foram prefixados com NamespaceA_ e NamespaceB_, respectivamente:

proj/foo/NamespaceA_Util.re
proj/bar/baz/NamespaceB_Util.re

Para tornar a nomeação menos pesada, há um módulo de namespace por namespace. O primeiro se parece com isso:

/* NamespaceA.re */
module Misc = NamespaceA_Misc;
module Util = NamespaceA_Util;

NamespaceA é usado da seguinte maneira:

/* Program.re */open NamespaceA;let x = Util.func();

O open nos permite usar Util sem um prefixo.

Há mais dois casos de uso para esta técnica:

  • Você pode substituir módulos com ele, até mesmo módulos da biblioteca padrão. Por exemplo, NamespaceA.re poderia conter uma implementação de List personalizada, que substituiria o módulo List padrão dentro de Program.Re:
module List = NamespaceA_List;
  • Você pode criar módulos aninhados enquanto mantém submódulos em arquivos separados. Por exemplo, além de abrir NamespaceA, você também pode acessar Util via NamespaceA.Util, porque ele está aninhado dentro NamespaceA. Claro, NamespaceA_Util funciona também, mas é desencorajado, porque é um detalhe de implementação.

A última técnica é usada por BuckleScript para JS.Date, JS.Promise, etc, no arquivo js.ml (que está na sintaxe OCaml):

···
module Date = Js_date
···
module Promise = Js_promise
···
module Console = Js_console

6.1 Módulos com namespacing em OCaml

Os namespaces de módulos são usados extensivamente em OCaml. Eles os chamam de módulos embalados, packed modules_, mas eu prefiro namespaces de módulos como nome, porque ele não conflita com o termo package do NPM.

Fonte desta seção: “Melhores espaços para nome através de aliases de módulo”, por Yaron Minsky para Jane Street Tech Blog.

7. Explorando a biblioteca padrão

Há duas grandes advertências anexadas à biblioteca padrão de ReasonML:

  • Atualmente os trabalhos estão em andamento.
  • Seu estilo de nomeação para os valores dentro de módulos vai mudar de snake-case (foo_bar e Foo_bar) para camel-case (fooBar e FooBar).
  • No momento, muita funcionalidade ainda está faltando.

7.1 Documentação da API

A biblioteca padrão do ReasonML é dividida: a maioria do núcleo da ReasonML API funciona em ambos, nativo e JavaScript (via BuckleScript). Se você compilar para JavaScript, você precisará usar a API do BuckleScript em dois casos:

  • Funcionalidade que está completamente ausente da API do ReasonML. Exemplos incluem suporte para datas, que você recebe via JS.Date do BuckleScript.
  • Funcionalidade de API do ReasonML que não é suportado pelo BuckleScript. Os exemplos incluem módulos Str (devido às seqüências de JavaScript sendo diferentes das nativas de ReasonML) e UNIX (com APIs nativas).

Esta é a documentação para as duas APIs:

7.2 Módulos Pervasives

Módulos Pervasives, contém a biblioteca padrão principal e é sempre aberta automaticamente para cada módulo. Ele contém funcionalidades como os operadores ==, +, |> e funções como print_string() e string_of_int().

Se algo neste módulo é sempre substituído, você ainda pode acessá-lo explicitamente através de, por exemplo, Pervasives.(+).

Se houver um arquivo Pervasives.re em seu projeto, ele substitui o módulo interno e é aberto em vez disso.

7.3 Funções padrão com parâmetros rotulados

Os módulos a seguir existem em duas versões: uma mais antiga, onde as funções têm apenas parâmetros posicionais e um mais recente, onde as funções também têm parâmetros rotulados.

  • Array, ArrayLabels
  • Bytes, BytesLabels
  • List, ListLabels
  • String, StringLabels

Um exemplo para considerar:

List.map: ('a => 'b, list('a)) => list('b)
ListLabels.map: (~f: 'a => 'b, list('a)) => list('b)

Mais dois módulos fornecem funções rotuladas:

  • O módulo StdLabels tem a matriz de submódulos, Array, Bytes, List, String, que são renomeadas para ArrayLabels etc. Em seus módulos, você pode abrir StdLabels para obter uma versão rotulada de List por padrão.
    O módulo MoreLabels tem três submódulos com funções rotuladas: Hashtbl, Map e Set.

8. Instalando bibliotecas

Por enquanto, JavaScript é a plataforma preferida para ReasonML. Portanto, a maneira preferida de instalar bibliotecas é via NPM. Como exemplo, suponha que queremos instalar as ligações BuckleScript para Jest (que incluem o próprio Jest). O pacote NPM relevante é chamado bs-jest.

Primeiro, precisamos instalar o pacote. Dentro de Package. JSON, você tem:

{
"dependencies": {
"bs-jest": "^0.1.5"
},
···
}

Em seguida, precisamos adicionar o pacote para bsconfig.json:

{
"bs-dependencies": [
"bs-jest"
],
···
}

Depois, podemos usar o módulo Jest com Jest.describe() etc.

Mais informações em como instalar bibliotecas:

9. Leitura adicional

Créditos

Written by

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store