Isso já faz parte 9 de uma série de artigos sobre programação funcional em F #! Estou certo de que em Habré não existem muitos ciclos tão longos. Mas não vamos parar. Hoje falaremos sobre funções aninhadas, módulos, espaços para nome e tipos e funções de mistura em módulos.

Agora você sabe como definir funções, mas como organizá-las?
O F # tem três opções:
- funções podem ser aninhadas em outras funções.
- no nível da aplicação, as funções de nível superior são agrupadas em "módulos".
- ou você pode seguir uma abordagem orientada a objetos e anexar funções aos tipos como métodos.
Neste artigo, consideraremos os dois primeiros métodos e o restante no próximo.
Funções aninhadas
No F #, você pode definir funções dentro de outras funções. Essa é uma boa maneira de encapsular funções auxiliares que são necessárias apenas para a função principal e não devem ser visíveis do lado de fora.
No exemplo abaixo, add
aninhado em addThreeNumbers
:
let addThreeNumbers xyz = // let add n = fun x -> x + n // x |> add y |> add z addThreeNumbers 2 3 4
Funções aninhadas podem acessar os parâmetros pai diretamente, porque estão em seu escopo.
Portanto, no exemplo abaixo, a função aninhada printError
não precisa de parâmetros, porque ela pode acessar n
max
diretamente.
let validateSize max n = // let printError() = printfn "Oops: '%i' is bigger than max: '%i'" n max // if n > max then printError() validateSize 10 9 validateSize 10 11
Um padrão muito comum é a função principal que define a função auxiliar recursiva aninhada, chamada com os valores iniciais correspondentes.
A seguir, é apresentado um exemplo desse código:
let sumNumbersUpTo max = // let rec recursiveSum n sumSoFar = match n with | 0 -> sumSoFar | _ -> recursiveSum (n-1) (n+sumSoFar) // recursiveSum max 0 sumNumbersUpTo 10
Tente evitar o aninhamento profundo, especialmente nos casos de acesso direto (não na forma de parâmetros) às variáveis pai.
Funções aninhadas excessivamente profundas serão tão difíceis de entender quanto a pior de muitas ramificações imperativas aninhadas.
Um exemplo de como não fazer:
// wtf, ? let fx = let f2 y = let f3 z = x * z let f4 z = let f5 z = y * z let f6 () = y * x f6() f4 y x * f2 x
Módulos
Um módulo é simplesmente uma coleção de funções agrupadas, geralmente porque elas funcionam com o mesmo tipo ou tipos de dados.
Uma definição de módulo é muito semelhante a uma definição de função. Começa com a palavra-chave module
, depois vem o sinal =
, seguido pelo conteúdo do módulo.
O conteúdo do módulo deve ser formatado com um deslocamento, bem como as expressões na definição de funções.
Definição de um módulo contendo duas funções:
module MathStuff = let add xy = x + y let subtract xy = x - y
Se você abrir esse código no Visual Studio, quando passar o mouse sobre add
poderá ver o nome completo add
, que na verdade é MathStuff.add
, como se MastStuff
fosse uma classe e add
fosse um método.
De fato, é exatamente isso que está acontecendo. Nos bastidores, o compilador F # cria uma classe estática com métodos estáticos. O equivalente em C # ficaria assim:
static class MathStuff { static public int add(int x, int y) { return x + y; } static public int subtract(int x, int y) { return x - y; } }
Reconhecer que os módulos são apenas classes estáticas e as funções são métodos estáticos fornecerão uma boa compreensão de como os módulos funcionam no F #, pois a maioria das regras que se aplicam às classes estáticas também se aplicam aos módulos.
E, assim como em C #, cada função autônoma deve fazer parte da classe, em F #, cada função autônoma deve fazer parte do módulo.
Acesso a funções fora do módulo
Se você precisar acessar uma função de outro módulo, poderá consultá-la através do nome completo.
module MathStuff = let add xy = x + y let subtract xy = x - y module OtherStuff = // MathStuff let add1 x = MathStuff.add x 1
Você também pode importar todas as funções de outro módulo usando a diretiva open
, após a qual você pode usar o nome abreviado em vez do completo.
module OtherStuff = open MathStuff // let add1 x = add x 1
As regras para o uso de nomes são bastante esperadas. Você sempre pode acessar uma função pelo nome completo ou pode usar nomes relativos ou incompletos, dependendo do escopo atual.
Módulos aninhados
Como as classes estáticas, os módulos podem conter módulos aninhados:
module MathStuff = let add xy = x + y let subtract xy = x - y // module FloatLib = let add xy :float = x + y let subtract xy :float = x - y
Outros módulos podem se referir a funções em módulos aninhados usando o nome completo ou relativo, conforme apropriado:
module OtherStuff = open MathStuff let add1 x = add x 1 // let add1Float x = MathStuff.FloatLib.add x 1.0 // let sub1Float x = FloatLib.subtract x 1.0
Módulos de nível superior
Assim, como os módulos podem ser aninhados, portanto, subindo a cadeia, é possível alcançar algum módulo pai do nível superior. É mesmo.
Os módulos de nível superior são definidos de maneira diferente, diferentemente dos módulos mostrados anteriormente.
- A
module MyModuleName
deve ser a primeira declaração no arquivo - Sinal
=
ausente - O conteúdo do módulo não deve ser recuado
Em geral, uma declaração de "nível superior" deve existir em cada arquivo .FS
origem. Existem algumas exceções, mas ainda é uma boa prática. O nome do módulo não precisa corresponder ao nome do arquivo, mas dois arquivos não podem conter módulos com o mesmo nome.
Para arquivos .FSX
, a declaração do módulo não é necessária; nesse caso, o nome do arquivo de script se torna automaticamente o nome do módulo.
Um exemplo de um MathStuff
declarado como um módulo "top module":
// module MathStuff let add xy = x + y let subtract xy = x - y // module FloatLib = let add xy :float = x + y let subtract xy :float = x - y
Observe que não há recuo no código de "nível superior" ( module MathStuff
), enquanto o conteúdo do módulo FloatLib
aninhado ainda precisa ser recuado.
Outro conteúdo do módulo
Além das funções, os módulos podem conter outras declarações, como declarações de tipo, valores simples e código de inicialização (por exemplo, construtores estáticos)
module MathStuff = // let add xy = x + y let subtract xy = x - y // type Complex = {r:float; i:float} type IntegerFunction = int -> int -> int type DegreesOrRadians = Deg | Rad // "" let PI = 3.141 // "" let mutable TrigType = Deg // / do printfn "module initialized"
A propósito, se você executar esses exemplos interativamente, talvez seja necessário reiniciar a sessão com frequência suficiente para que o código permaneça “atualizado” e não seja infectado por cálculos anteriores.
Ocultação (sobreposição, sombreamento)
Este é novamente o nosso módulo de amostra. Observe que MathStuff
contém a função add
, bem como FloatLib
.
module MathStuff = let add xy = x + y let subtract xy = x - y // module FloatLib = let add xy :float = x + y let subtract xy :float = x - y
O que acontece se você abrir os dois módulos no escopo atual e chamar add
?
open MathStuff open MathStuff.FloatLib let result = add 1 2 // Compiler error: This expression was expected to // have type float but here has type int
E aconteceu que o módulo MathStuff.FloatLib
redefiniu o MathStuff
original, que foi bloqueado (oculto) pelo módulo FloatLib
.
Como resultado, obtemos o erro do compilador FS0001, porque o primeiro parâmetro 1
era esperado como flutuador. Para corrigir isso, você deve alterar 1
para 1.0
.
Infelizmente, na prática, isso é discreto e facilmente esquecido. Às vezes, usando essa técnica, você pode fazer truques interessantes, quase como subclasses, mas na maioria das vezes a presença de funções com o mesmo nome é irritante (por exemplo, no caso da função de map
extremamente comum).
Se você deseja evitar esse comportamento, existe uma maneira de interrompê-lo com o atributo RequireQualifiedAccess
. O mesmo exemplo no qual os dois módulos são decorados com este atributo:
[<RequireQualifiedAccess>] module MathStuff = let add xy = x + y let subtract xy = x - y // [<RequireQualifiedAccess>] module FloatLib = let add xy :float = x + y let subtract xy :float = x - y
Agora a diretiva open
não está disponível:
open MathStuff // open MathStuff.FloatLib //
Mas você ainda pode acessar funções (sem nenhuma ambiguidade) através de seus nomes completos:
let result = MathStuff.add 1 2 let result = MathStuff.FloatLib.add 1.0 2.0
Controle de acesso
O F # suporta o uso de operadores de controle de acesso .NET padrão, como public
, private
e internal
. O artigo MSDN contém informações completas.
- Esses especificadores de acesso podem ser aplicados a ("deixar ligado") funções, valores, tipos e outras declarações de nível superior em um módulo. Eles também podem ser especificados para os próprios módulos (por exemplo, um módulo aninhado particular pode ser necessário).
- Por padrão, tudo tem acesso público (com exceção de vários casos); portanto, para protegê-los, você precisará usar
private
ou internal
.
Esses especificadores de acesso são apenas uma maneira de controlar a visibilidade em F #. Uma maneira completamente diferente é usar arquivos de assinatura semelhantes aos arquivos de cabeçalho C. Eles descrevem abstratamente o conteúdo do módulo. As assinaturas são muito úteis para encapsulamento sério, mas, para considerar seus recursos, você deverá aguardar a série planejada de encapsulamento e segurança com base nos recursos .
Namespaces
Os espaços para nome no F # são semelhantes aos espaços de nome do C #. Eles podem ser usados para organizar módulos e tipos para evitar conflitos de nome.
Um namespace declarado usando a palavra-chave namespace
:
namespace Utilities module MathStuff = // let add xy = x + y let subtract xy = x - y
Devido a esse espaço para nome, o nome completo do módulo MathStuff
tornou Utilities.MathStuff
e o nome completo add
é Utilities.MathStuff.add
.
As mesmas regras de indentação se aplicam aos módulos em um espaço para nome que foram mostrados acima para os módulos.
Você também pode declarar um espaço para nome explicitamente adicionando um ponto no nome do módulo. I.e. O código acima pode ser reescrito assim:
module Utilities.MathStuff // let add xy = x + y let subtract xy = x - y
O nome completo do módulo MathStuff
ainda é Utilities.MathStuff
, mas agora é um módulo de nível superior e seu conteúdo não precisa de indentação.
Alguns recursos adicionais para o uso de espaços para nome:
- Os espaços para nome são opcionais para os módulos. Diferentemente do C #, para projetos de F # não há espaço para nome padrão, portanto, um módulo de nível superior sem um espaço para nome será global. Se você planeja criar bibliotecas reutilizáveis, adicione vários espaços para nome para evitar conflitos com o código de outras bibliotecas.
- Os espaços para nome podem conter diretamente declarações de tipo, mas não declarações de função. Como observado anteriormente, todas as declarações de função e valor devem fazer parte de um módulo.
- Por fim, lembre-se de que os espaços para nome não funcionam em scripts. Por exemplo, se você tentar enviar uma declaração de espaço para nome, como
namespace Utilities
para namespace Utilities
para namespace Utilities
, para uma janela interativa, um erro será recebido.
Hierarquia de namespace
Você pode criar uma hierarquia de namespaces simplesmente dividindo os nomes com pontos:
namespace Core.Utilities module MathStuff = let add xy = x + y
Você também pode declarar dois namespaces em um arquivo, se desejar. Deve-se observar que todos os namespaces devem ser declarados pelo nome completo - eles não suportam aninhamento.
namespace Core.Utilities module MathStuff = let add xy = x + y namespace Core.Extra module MoreMathStuff = let add xy = x + y
Um conflito de nome entre o espaço para nome e o módulo não é possível.
namespace Core.Utilities module MathStuff = let add xy = x + y namespace Core // - Core.Utilities // ! module Utilities = let add xy = x + y
Misturando tipos e funções em módulos
Como vimos, os módulos geralmente consistem em muitas funções interdependentes que interagem com um tipo de dados específico.
No POO, estruturas e funções de dados acima deles seriam combinadas em uma classe. E no F # funcional, estruturas de dados e funções acima deles são combinadas em um módulo.
Existem dois padrões para combinar tipos e funções:
- tipo é declarado separadamente das funções
- O tipo é declarado no mesmo módulo que as funções
No primeiro caso, o tipo é declarado fora de qualquer módulo (mas no espaço para nome), após o qual as funções que funcionam com esse tipo são colocadas no módulo do mesmo tipo.
// namespace Example // type PersonType = {First:string; Last:string} // , module Person = // let create first last = {First=first; Last=last} // , let fullName {First=first; Last=last} = first + " " + last let person = Person.create "john" "doe" Person.fullName person |> printfn "Fullname=%s"
Como alternativa, o tipo é declarado dentro do módulo e possui um nome simples como " T
" ou o nome do módulo. O acesso às funções é aproximadamente o seguinte: MyModule.Func
e MyModule.Func2
e acesso ao tipo: MyModule.T
:
module Customer = // Customer.T - type T = {AccountId:int; Name:string} // let create id name = {T.AccountId=id; T.Name=name} // , let isValid {T.AccountId=id; } = id > 0 let customer = Customer.create 42 "bob" Customer.isValid customer |> printfn "Is valid?=%b"
Observe que, nos dois casos, deve haver uma função construtora que crie uma nova instância do tipo (factory). Então, no código do cliente, você dificilmente precisa acessar o nome do tipo explicitamente e não precisa se perguntar se o tipo está dentro do módulo ou não.
Então, qual o caminho a escolher?
- A primeira abordagem é mais parecida com o .NET clássico e deve ser preferida se você planeja usar essa biblioteca para código fora do F #, onde uma classe existente separadamente é esperada.
- A segunda abordagem é mais comum em outras linguagens funcionais. O tipo dentro do módulo é compilado como uma classe aninhada, o que geralmente não é muito conveniente para idiomas OOP.
Para si mesmo, você pode experimentar os dois métodos. No caso do desenvolvimento da equipe, um estilo deve ser escolhido.
Módulos que contêm apenas tipos
Se houver muitos tipos que precisam ser declarados sem nenhuma função, não se preocupe em usar o módulo. Você pode declarar tipos diretamente no espaço para nome sem recorrer a classes aninhadas.
Por exemplo, você pode querer fazer isso:
// module Example // type PersonType = {First:string; Last:string} // , ...
E aqui está outra maneira de fazer o mesmo. O module
word module
simplesmente substituído pelo namespace
da palavra.
// namespace Example // type PersonType = {First:string; Last:string}
Nos dois casos, PersonType
terá o mesmo nome completo.
Observe que essa substituição funciona apenas com tipos. As funções sempre devem ser declaradas dentro do módulo.
Recursos Adicionais
Existem muitos tutoriais para F #, incluindo materiais para quem vem com experiência em C # ou Java. Os links a seguir podem ser úteis à medida que você avança no F #:
Várias outras maneiras de começar a aprender F # também são descritas.
Finalmente, a comunidade F # é muito amigável para iniciantes. Há um bate-papo muito ativo no Slack, suportado pela F # Software Foundation, com salas para iniciantes nas quais você pode participar livremente . É altamente recomendável que você faça isso!
Não se esqueça de visitar o site da comunidade de língua russa F # ! Se você tiver alguma dúvida sobre o aprendizado de um idioma, teremos prazer em discuti-los nas salas de bate-papo:
Sobre autores de tradução
Traduzido por @kleidemos
As mudanças de tradução e editoriais foram feitas pelos esforços da comunidade de desenvolvedores de F # de língua russa . Agradecemos também a @schvepsss e @shwars pela preparação deste artigo para publicação.