Compartilhar via


Diretrizes de design de componentes F#

Este documento é um conjunto de diretrizes de design de componentes para programação F#, com base nas Diretrizes de Design de Componentes do F#, v14, Microsoft Research e uma versão que foi originalmente selecionada e mantida pelo F# Software Foundation.

Este documento pressupõe que você esteja familiarizado com a programação F#. Muito obrigado à comunidade F# por suas contribuições e comentários úteis sobre várias versões deste guia.

Visão geral

Este documento analisa alguns dos problemas relacionados ao design e à codificação de componentes F#. Um componente pode significar qualquer um dos seguintes:

  • Uma camada em seu projeto F# que tem consumidores externos dentro desse projeto.
  • Uma biblioteca destinada ao consumo com código em F# nos limites do assembly.
  • Uma biblioteca destinada ao consumo por qualquer linguagem .NET nos limites do assembly.
  • Uma biblioteca destinada à distribuição por meio de um repositório de pacotes, como nuGet.

As técnicas descritas neste artigo seguem os Cinco princípios de um bom código em F# e, portanto, utilizam programação funcional e de objeto, conforme apropriado.

Independentemente da metodologia, o designer de componentes e bibliotecas enfrenta uma série de problemas práticos e prosaicos ao tentar criar uma API que seja mais facilmente utilizável pelos desenvolvedores. A aplicação consciente das diretrizes de design da biblioteca do .NET orientará você a criar um conjunto consistente de APIs que são agradáveis de consumir.

Diretrizes gerais

Há algumas diretrizes universais que se aplicam às bibliotecas F#, independentemente do público-alvo pretendido para a biblioteca.

Conheça as diretrizes de design da biblioteca do .NET

Independentemente do tipo de codificação F# que você está fazendo, é importante ter um conhecimento funcional das diretrizes de design da biblioteca do .NET . A maioria dos outros programadores F# e .NET estará familiarizada com essas diretrizes e espera que o código .NET esteja em conformidade com elas.

As Diretrizes de Design da Biblioteca do .NET fornecem diretrizes gerais sobre nomenclatura, design de classes e interfaces, design de membro (propriedades, métodos, eventos etc.) e muito mais e são um primeiro ponto de referência útil para uma variedade de diretrizes de design.

Adicionar comentários de documentação XML ao seu código

A documentação XML sobre APIs públicas garante que os usuários possam obter excelentes Intellisense e Quickinfo ao utilizar esses tipos e membros, além de possibilitar a criação de arquivos de documentação para a biblioteca. Consulte a documentação XML sobre várias tags XML que podem ser usadas para marcação adicional nos comentários xmldoc.

/// A class for representing (x,y) coordinates
type Point =

    /// Computes the distance between this point and another
    member DistanceTo: otherPoint:Point -> float

Você pode usar os comentários XML de formato curto (/// comment) ou comentários XML padrão (///<summary>comment</summary>).

Considere usar arquivos de assinatura explícitos (.fsi) para APIs estáveis de biblioteca e componente

O uso de arquivos de assinaturas explícitas em uma biblioteca F# fornece um resumo sucinto da API pública, que ajuda a garantir que você conheça a superfície pública completa da biblioteca e forneça uma separação limpa entre a documentação pública e os detalhes da implementação interna. Os arquivos de assinatura adicionam atrito à alteração da API pública, exigindo que sejam feitas alterações nos arquivos de implementação e de assinatura. Como resultado, os arquivos de assinatura normalmente só devem ser introduzidos quando uma API se solidificou e não é mais esperado que mude significativamente.

Siga as práticas recomendadas para usar cadeias de caracteres no .NET

Siga as diretrizes de Melhores práticas para o uso de cadeias de caracteres no .NET quando o escopo do projeto justificar isso. Em particular, declarar explicitamente a intenção cultural na conversão e comparação de cadeias de caracteres (em que for aplicável).

Diretrizes para bibliotecas voltadas para F#

Esta seção apresenta recomendações para o desenvolvimento de bibliotecas públicas voltadas para F#; ou seja, bibliotecas que expõem APIs públicas destinadas a serem consumidas por desenvolvedores F#. Há uma variedade de recomendações de design de biblioteca aplicáveis especificamente ao F#. Na ausência das recomendações específicas, recorra às Diretrizes de Design de Biblioteca .NET.

Convenções de nomenclatura

Usar convenções de nomenclatura e capitalização do .NET

A tabela a seguir segue as convenções de nomenclatura e capitalização do .NET. Há pequenas adições para incluir também constructos F#. Essas recomendações são especialmente destinadas para APIs que ultrapassam os limites de F# para F#, adequando-se aos idiomas do .NET BCL e da maioria das bibliotecas.

Construir Caso Parte Exemplos Anotações
Tipos concretos PascalCase Substantivo/adjetivo List, Double, Complex Tipos concretos são structs, classes, enumerações, representantes, registros e uniões. Embora os nomes de tipo sejam tradicionalmente minúsculos no OCaml, o F# adotou o esquema de nomenclatura do .NET para tipos.
DLLs PascalCase Fabrikam.Core.dll
Marcas de união PascalCase Substantivo Some, Add, Success Não use um prefixo em APIs públicas. Opcionalmente, use um prefixo quando interno, como "type Teams = TAlpha | Eta | Fulano".
Acontecimento PascalCase Verbo ValueChanged/ValueChanging
Exceções PascalCase WebException O nome deve terminar com "Exceção".
Campo PascalCase Substantivo NomeAtual
Tipos de interface PascalCase Substantivo/adjetivo IDisposable O nome deve começar com "I".
Método PascalCase Verbo ToString
Namespace PascalCase Microsoft.FSharp.Core A recomendação é usar <Organization>.<Technology>[.<Subnamespace>] no geral, removendo a organização caso a tecnologia seja independente dela.
Parâmetros camelCase Substantivo typeName, transform, range
let values (interno) camelCase ou PascalCase Substantivo/verbo getValue, myTable
let values (externo) camelCase ou PascalCase Substantivo/verbo List.map, Dates.Today Geralmente, ao seguir padrões de design funcionais tradicionais, os valores let-bound são públicos. No entanto, geralmente, use PascalCase quando o identificador pode ser usado a partir de outras linguagens do .NET.
Propriedade PascalCase Substantivo/adjetivo IsEndOfFile, BackColor As propriedades boolianas geralmente usam Is e Can e devem ser afirmativas, como em IsEndOfFile, não IsNotEndOfFile.

Evitar abreviações

As diretrizes do .NET desencorajam o uso de abreviações (por exemplo, "use OnButtonClick em vez de OnBtnClick"). Abreviações comuns, como Async para "Assíncrono", são toleradas. Às vezes, essa diretriz é ignorada para programação funcional; por exemplo, List.iter usa uma abreviação para "iterar". Por esse motivo, o uso de abreviações tende a ser tolerado em maior grau na programação F#to-F#, mas ainda deve ser geralmente evitado no design de componentes públicos.

Evitar colisões de uso de maiúsculas e minúsculas em nomes

Segundo as diretrizes do .NET, não é possível fazer uso de maiúsculas e minúsculas para desambiguar colisões de nomes, pois algumas linguagens de cliente (por exemplo, Visual Basic) não diferenciam maiúsculas de minúsculas.

Usar acrônimos quando apropriado

Acrônimos como XML não são abreviações e são amplamente usados em bibliotecas .NET em formato nãopitalizado (Xml). Apenas acrônimos conhecidos e amplamente reconhecidos devem ser usados.

Usar PascalCase para nomes de parâmetro genéricos

Use PascalCase para nomes de parâmetro genéricos em APIs públicas, inclusive para bibliotecas voltadas para F#. Em especial, use nomes como T, U, T1, T2 para parâmetros genéricos arbitrários e, quando nomes específicos fizerem sentido, para bibliotecas voltadas para F# use nomes como Key, Value, Arg (mas não, por exemplo, TKey).

Use PascalCase ou camelCase para funções públicas e valores em módulos F#

camelCase é usado para funções públicas que são projetadas para serem usadas sem qualificação (por exemplo, invalidArg) e para as "funções de coleção padrão" (por exemplo, List.map). Nesses dois casos, os nomes de função agem muito semelhantes às palavras-chave no idioma.

Design de objeto, tipo e módulo

Usar namespaces ou módulos para conter tipos e módulos

Cada arquivo F# em um componente deve começar com uma declaração de namespace ou uma declaração de módulo.

namespace Fabrikam.BasicOperationsAndTypes

type ObjectType1() =
    ...

type ObjectType2() =
     ...

module CommonOperations =
    ...

or

module Fabrikam.BasicOperationsAndTypes

type ObjectType1() =
    ...

type ObjectType2() =
    ...

module CommonOperations =
    ...

As diferenças entre o uso de módulos e namespaces para organizar o código no nível superior são as seguintes:

  • Namespaces podem abranger vários arquivos
  • Os namespaces não podem conter funções F#, a menos que estejam dentro de um módulo interno
  • O código de qualquer módulo determinado deve estar contido em um único arquivo
  • Módulos de nível superior podem conter funções F# sem a necessidade de um módulo interno

A escolha entre um namespace ou módulo de nível superior afeta a forma compilada do código e, portanto, afetará a exibição de outros idiomas do .NET caso sua API eventualmente seja consumida fora do código F#.

Usar métodos e propriedades para operações intrínsecas a tipos de objeto

Ao trabalhar com objetos, é melhor garantir que a funcionalidade consumível seja implementada como métodos e propriedades nesse tipo.

type HardwareDevice() =

    member this.ID = ...

    member this.SupportedProtocols = ...

type HashTable<'Key,'Value>(comparer: IEqualityComparer<'Key>) =

    member this.Add(key, value) = ...

    member this.ContainsKey(key) = ...

    member this.ContainsValue(value) = ...

A maior parte da funcionalidade de um determinado membro não precisa necessariamente ser implementada nele, mas a parcela consumível da funcionalidade precisa.

Usar classes para encapsular o estado mutável

Em F#, isso só precisa ser feito quando esse estado ainda não estiver encapsulado por outro constructo de linguagem, como um fechamento, uma expressão de sequência ou uma computação assíncrona.

type Counter() =
    // let-bound values are private in classes.
    let mutable count = 0

    member this.Next() =
        count <- count + 1
        count

Use tipos de interface para representar um conjunto de operações. Isso é preferido a outras opções, como tuplas de funções ou registros de funções.

type Serializer =
    abstract Serialize<'T> : preserveRefEq: bool -> value: 'T -> string
    abstract Deserialize<'T> : preserveRefEq: bool -> pickle: string -> 'T

Dê preferência com relação ao seguinte:

type Serializer<'T> = {
    Serialize: bool -> 'T -> string
    Deserialize: bool -> string -> 'T
}

As interfaces são conceitos de primeira classe no .NET, que você pode usar para alcançar o que os Functors normalmente lhe dariam. Além disso, eles podem ser usados para codificar tipos existenciais em seu programa, o que as estruturas de funções não conseguem fazer.

Usar um módulo para agrupar funções que atuam em coleções

Ao definir um tipo de coleção, considere fornecer um conjunto padrão de operações como CollectionType.map e CollectionType.iter) para novos tipos de coleção.

module CollectionType =
    let map f c =
        ...
    let iter f c =
        ...

Se você incluir esse módulo, siga as convenções de nomenclatura padrão para funções encontradas no FSharp.Core.

Usar um módulo para agrupar funções para funções comuns e canônicas, especialmente em bibliotecas de matemática e DSL

Por exemplo, Microsoft.FSharp.Core.Operators é uma coleção aberta automaticamente de funções de nível superior (como abs e sin) fornecida pelo FSharp.Core.dll.

Da mesma forma, uma biblioteca de estatísticas pode incluir um módulo com funções erf e erfc, em que este módulo foi projetado para ser aberto explicitamente ou automaticamente.

Considere usar RequireQualifiedAccess e aplicar cuidadosamente atributos AutoOpen

Adicionar o atributo [<RequireQualifiedAccess>] a um módulo indica que o módulo pode não ser aberto e que as referências aos elementos do módulo exigem acesso qualificado explícito. Por exemplo, o módulo Microsoft.FSharp.Collections.List tem esse atributo.

Isso é útil quando funções e valores no módulo têm nomes que provavelmente entrarão em conflito com nomes em outros módulos. Exigir acesso qualificado pode aumentar consideravelmente a manutenção e a evolução de uma biblioteca a longo prazo.

É altamente sugerido ter o atributo [<RequireQualifiedAccess>] para módulos personalizados que estendem os fornecidos por FSharp.Core (como Seq, List, Array), pois esses módulos são usados predominantemente no código F# e têm [<RequireQualifiedAccess>] definidos neles; de modo mais geral, não é recomendável definir módulos personalizados sem o atributo, quando esses módulos são sombreados ou estendem outros módulos que têm o atributo.

Adicionar o atributo [<AutoOpen>] a um módulo significa que o módulo será aberto quando o namespace que o contém for aberto. O atributo [<AutoOpen>] também pode ser aplicado a um assembly para indicar um módulo que é aberto automaticamente quando o assembly é referenciado.

Por exemplo, uma biblioteca de estatísticas MathsHeaven.Statistics pode conter um module MathsHeaven.Statistics.Operators que contém funções erf e erfc. É razoável marcar este módulo como [<AutoOpen>]. Isso significa que open MathsHeaven.Statistics também abrirá este módulo e trará os nomes erf e erfc para o escopo. Outro bom uso de [<AutoOpen>] é para módulos que contêm métodos de extensão.

O uso excessivo de [<AutoOpen>] leva a namespaces poluídos e o atributo deve ser usado com cuidado. Para bibliotecas específicas em domínios específicos, o uso criterioso de [<AutoOpen>] pode levar a uma melhor usabilidade.

Considere definir membros do operador em classes em que o uso de operadores conhecidos é apropriado

Às vezes, as classes são usadas para modelar construções matemáticas, como vetores. Quando o domínio que está sendo modelado tem operadores conhecidos, defini-los como membros intrínsecos à classe é útil.

type Vector(x: float) =

    member v.X = x

    static member (*) (vector: Vector, scalar: float) = Vector(vector.X * scalar)

    static member (+) (vector1: Vector, vector2: Vector) = Vector(vector1.X + vector2.X)

let v = Vector(5.0)

let u = v * 10.0

Essa orientação corresponde às diretrizes gerais do .NET para esses tipos. No entanto, ele pode ser adicionalmente importante na codificação F#, pois isso permite que esses tipos sejam usados em conjunto com funções e métodos F# com restrições de membro, como List.sumBy.

Considere usar CompiledName para fornecer um nome amigável do .NET para outros consumidores de linguagem do .NET

Às vezes, é possível nomear algo em um estilo para os consumidores de F# (como um membro estático em letras minúsculas que aparece como se fosse uma função vinculada ao módulo), mas precisar de um estilo diferente para o nome quando ele é compilado em um assembly. Você pode usar o atributo [<CompiledName>] para fornecer um estilo diferente para o código não F# que consome o assembly.

type Vector(x:float, y:float) =

    member v.X = x
    member v.Y = y

    [<CompiledName("Create")>]
    static member create x y = Vector (x, y)

let v = Vector.create 5.0 3.0

Ao utilizar [<CompiledName>], você pode aplicar convenções de nomenclatura do .NET para consumidores do assembly que não sejam usuários de F#.

Usar a sobrecarga de método para funções de membro, o que fornece uma API mais simples

A sobrecarga de método é uma ferramenta poderosa para simplificar uma API que pode precisar executar funcionalidades semelhantes, mas com diferentes opções ou argumentos.

type Logger() =

    member this.Log(message) =
        ...
    member this.Log(message, retryPolicy) =
        ...

Em F#, é mais comum sobrecarregar o número de argumentos em vez de tipos de argumentos.

Ocultar as representações de tipos de união e registro quando houver probabilidade de aprimoramento do design desses tipos

Evite revelar representações concretas de objetos. Por exemplo, a representação concreta de valores DateTime não é revelada pela API externa e pública do projeto da biblioteca .NET. Em tempo de execução, o Common Language Runtime conhece a implementação confirmada que será usada durante toda a execução. No entanto, o código compilado não capta dependências na representação concreta.

Evitar o uso de herança de implementação para extensibilidade

Em F#, a herança de implementação raramente é usada. Além disso, as hierarquias de herança geralmente são complexas e difíceis de alterar quando novos requisitos chegam. A implementação da herança ainda existe em F# para fins de compatibilidade e casos raros em que ela é a melhor solução para um problema, mas é recomendado procurar técnicas alternativas nos programas em F# ao realizar designs para polimorfismo, como implementações de interface.

Assinaturas de membro e função

Use tuplas para valores de retorno ao retornar um pequeno número de vários valores não relacionados

Veja o seguinte exemplo de uso de uma tupla em um tipo de retorno:

val divrem: BigInteger -> BigInteger -> BigInteger * BigInteger

Para tipos de retorno com muitos componentes variados ou que estão relacionados a uma única entidade identificável, considere usar um tipo nomeado em vez de uma tupla.

Usar Async<T> para programação assíncrona nos limites da API F#

Se houver uma operação síncrona correspondente chamada Operation que retorna um T, a operação assíncrona deverá ser nomeada AsyncOperation se retornar Async<T> ou OperationAsync se retornar Task<T>. Para tipos .NET comumente usados que expõem métodos Begin/End, considere usar Async.FromBeginEnd para escrever métodos de extensão como uma fachada para fornecer o modelo de programação assíncrona F# para essas APIs do .NET.

type SomeType =
    member this.Compute(x:int): int =
        ...
    member this.AsyncCompute(x:int): Async<int> =
        ...

type System.ServiceModel.Channels.IInputChannel with
    member this.AsyncReceive() =
        ...

Exceções

Consulte o Gerenciamento de Erros para saber sobre o uso apropriado de exceções, resultados e opções.

Membros da Extensão

Aplique cuidadosamente os membros de extensão de F# em componentes de F# para F#

Geralmente, os membros de extensão de F# devem ser usados somente para operações no encerramento de operações intrínsecas associadas a um tipo na maioria dos respectivos modos de uso. Um uso comum é fornecer APIs mais idiomáticas para F# para vários tipos de .NET:

type System.ServiceModel.Channels.IInputChannel with
    member this.AsyncReceive() =
        Async.FromBeginEnd(this.BeginReceive, this.EndReceive)

type System.Collections.Generic.IDictionary<'Key,'Value> with
    member this.TryGet key =
        let ok, v = this.TryGetValue key
        if ok then Some v else None

Tipos de união

Usar uniões discriminadas em vez de hierarquias de classe para dados estruturados em árvore

Estruturas semelhantes a árvores são definidas recursivamente. Isso pode ser estranho com a herança, mas é muito elegante no caso das uniões discriminadas.

type BST<'T> =
    | Empty
    | Node of 'T * BST<'T> * BST<'T>

A representação de dados semelhantes a árvores com uniões discriminadas também permite aproveitar a correspondência detalhada de padrões.

Usar [<RequireQualifiedAccess>] em tipos de união com nomes de caso insuficientemente exclusivos

Você pode se encontrar em um domínio em que o mesmo nome é o melhor nome para coisas diferentes, como casos de União Discriminada. É possível usar [<RequireQualifiedAccess>] para desambiguar os nomes de casos a fim de evitar o disparo de erros confusos devido ao sombreamento dependente da ordenação das instruções open

Oculte as representações de uniões discriminadas para APIs binariamente compatíveis se o design desses tipos for suscetível a evoluir.

Os tipos de união utilizam os formulários de correspondência de padrões de F# para obter um modelo de programação sucinto. Conforme mencionado anteriormente, você deve evitar revelar representações concretas de dados caso o design desses tipos provavelmente evolua.

Por exemplo, a representação de uma união discriminada pode ser ocultada usando uma declaração privada ou interna ou usando um arquivo de assinatura.

type Union =
    private
    | CaseA of int
    | CaseB of string

Se você revelar uniões discriminadas sem qualquer tipo de controle, poderá ter dificuldades para versionar a biblioteca sem invalidar o código do usuário. Em vez disso, considere revelar um ou mais padrões ativos a fim de permitir a correspondência de padrões em relação aos valores do seu tipo.

Os padrões ativos oferecem uma maneira alternativa de fornecer a correspondência de padrões aos consumidores de F#, evitando a exposição direta dos tipos de união de F#.

Restrições de membros e funções Inline

Definir algoritmos numéricos genéricos usando funções embutidas com restrições de membro implícitas e tipos genéricos resolvidos estaticamente

As restrições de membros aritméticos e as restrições de comparação em F# são um padrão para a programação em F#. Por exemplo, considere o seguinte código:

let inline highestCommonFactor a b =
    let rec loop a b =
        if a = LanguagePrimitives.GenericZero<_> then b
        elif a < b then loop a (b - a)
        else loop (a - b) b
    loop a b

O tipo dessa função é o seguinte:

val inline highestCommonFactor : ^T -> ^T -> ^T
                when ^T : (static member Zero : ^T)
                and ^T : (static member ( - ) : ^T * ^T -> ^T)
                and ^T : equality
                and ^T : comparison

Essa é uma função adequada para uma API pública em uma biblioteca matemática.

Evitar o uso de restrições de membro para simular classes de tipo e duck typing

É possível simular o "duck typing" usando restrições de membros em F#. No entanto, os membros que fazem uso dessa opção geralmente não devem ser usados em designs de biblioteca de F# para F#. Isso ocorre porque designs de biblioteca baseados em restrições implícitas desconhecidas ou não padrão tendem a fazer com que o código do usuário se torne inflexível e vinculado a um padrão de estrutura específico.

Além disso, há uma boa chance de que o uso extensivo de restrições entre membros dessa forma possa resultar em tempos de compilação extremamente longos.

Definições de operador

Evite definir operadores simbólicos personalizados

Os operadores personalizados são essenciais em algumas situações e são dispositivos notacionais altamente úteis dentro de um grande corpo de código de implementação. Para novos usuários de uma biblioteca, as funções nomeadas geralmente são mais fáceis de usar. Além disso, os operadores simbólicos personalizados podem ser difíceis de documentar e os usuários acham mais difícil procurar ajuda nos operadores devido às limitações existentes no IDE e mecanismos de pesquisa.

Como resultado, é melhor publicar sua funcionalidade na forma de funções e membros nomeados e, adicionalmente, expor operadores para ela somente caso os benefícios de notação superem a necessidade de documentação e o custo cognitivo relacionados.

Unidades de Medida

Usar cuidadosamente unidades de medida para adicionar segurança de tipo no código F#

Informações adicionais de digitação para unidades de medida são apagadas quando exibidas por outros idiomas do .NET. Esteja ciente de que os componentes, as ferramentas e a reflexão do .NET verão types-sans-units. Por exemplo, os consumidores de C# verão float em vez de float<kg>.

Abreviações de tipo

Usar cuidadosamente abreviações de tipo para simplificar o código F#

Componentes do .NET, ferramentas e reflexão não utilizarão nomes abreviados para tipos. O uso significativo de abreviações de tipo também pode fazer com que um domínio pareça mais complexo do que realmente é, o que pode confundir os consumidores.

Evite abreviações de tipo para tipos públicos cujos membros e propriedades devem ser intrinsecamente diferentes daqueles disponíveis no tipo que está sendo abreviado

Nesse caso, o tipo que está sendo abreviado revela muito sobre a representação do tipo real que está sendo definido. Em vez disso, considere agrupar a abreviação em um tipo de classe ou em uma união discriminada de caso único (ou, quando o desempenho for essencial, considere usar um tipo de estrutura para agrupar a abreviação).

Por exemplo, é tentador definir um mapa múltiplo como um caso especial de um mapa F#, por exemplo:

type MultiMap<'Key,'Value> = Map<'Key,'Value list>

No entanto, as operações lógicas de notação de ponto nesse tipo não são as mesmas que as operações em um mapa– por exemplo, é razoável que o operador de pesquisa map[key] retornar a lista vazia se a chave não estiver no dicionário, em vez de gerar uma exceção.

Diretrizes para bibliotecas para uso de outras linguagens do .NET

Ao criar bibliotecas para uso de outras linguagens do .NET, é importante aderir às diretrizes de design da biblioteca do .NET . Neste documento, essas bibliotecas são rotuladas como bibliotecas .NET padrão, como contraponto às bibliotecas voltadas para F# que usam construtos F# sem limitações. Criar bibliotecas .NET padrão significa fornecer APIs familiares e idiomáticas consistentes com o restante do framework .NET, minimizando o uso de constructos específicos de F# na API pública. As regras são explicadas nas seções a seguir.

Design de namespace e tipo (para bibliotecas que serão usadas por outras linguagens .NET)

Aplicar as convenções de nomenclatura do .NET à API pública de seus componentes

Preste atenção especial ao uso de nomes abreviados e às diretrizes de capitalização do .NET.

type pCoord = ...
    member this.theta = ...

type PolarCoordinate = ...
    member this.Theta = ...

Usar namespaces, tipos e membros como a estrutura organizacional primária para seus componentes

Todos os arquivos que contêm funcionalidade pública devem começar com uma declaração namespace e as únicas entidades voltadas para o público em namespaces devem ser tipos. Não use módulos F#.

Use módulos não públicos para conter código de implementação, tipos de utilitário e funções de utilitário.

Os tipos estáticos devem ser preferenciais em vez de módulos, pois permitem que a evolução futura da API use sobrecarga e outros conceitos de design de API do .NET que podem não ser usados em módulos F#.

Por exemplo, no lugar da seguinte API pública:

module Fabrikam

module Utilities =
    let Name = "Bob"
    let Add2 x y = x + y
    let Add3 x y z = x + y + z

Em vez disso, considere:

namespace Fabrikam

[<AbstractClass; Sealed>]
type Utilities =
    static member Name = "Bob"
    static member Add(x,y) = x + y
    static member Add(x,y,z) = x + y + z

Usar tipos de registro de F# em APIs básicas do .NET se o design dos tipos não for aprimorado

Os tipos de registro F# são compilados em uma classe .NET simples. Elas são adequadas para alguns tipos simples e estáveis em APIs. Considere usar os atributos [<NoEquality>] e [<NoComparison>] para suprimir a geração automática de interfaces. Evite também usar campos de registro mutáveis em APIs .NET padrão, pois eles expõem um campo público. Sempre considere se uma classe forneceria uma opção mais flexível para a evolução futura da API.

Por exemplo, o código F# a seguir expõe a API pública a um consumidor em C#:

F#:

[<NoEquality; NoComparison>]
type MyRecord =
    { FirstThing: int
        SecondThing: string }

C#:

public sealed class MyRecord
{
    public MyRecord(int firstThing, string secondThing);
    public int FirstThing { get; }
    public string SecondThing { get; }
}

Ocultar a representação de tipos de união de F# em APIs básicas do .NET

Os tipos de união de F# não são comumente usados em limites de componente, mesmo para codificação de F# para F#. Eles são um excelente dispositivo de implementação quando usado internamente em componentes e bibliotecas.

Ao projetar uma API .NET simples, considere ocultar a representação de um tipo de união usando uma declaração privada ou um arquivo de assinatura.

type PropLogic =
    private
    | And of PropLogic * PropLogic
    | Not of PropLogic
    | True

Também é possível aumentar tipos que usam uma representação de união internamente com membros para fornecer uma API desejada voltada ao .NET.

type PropLogic =
    private
    | And of PropLogic * PropLogic
    | Not of PropLogic
    | True

    /// A public member for use from C#
    member x.Evaluate =
        match x with
        | And(a,b) -> a.Evaluate && b.Evaluate
        | Not a -> not a.Evaluate
        | True -> true

    /// A public member for use from C#
    static member CreateAnd(a,b) = And(a,b)

Criar GUI e outros componentes usando os padrões de design da estrutura

Há muitas estruturas diferentes disponíveis no .NET, como WinForms, WPF e ASP.NET. Convenções de nomenclatura e design para cada uma delas devem ser usadas se você estiver projetando componentes para uso nessas estruturas. Por exemplo, para programação WPF, adote padrões de design do WPF para as classes que você está projetando. Para modelos na programação de interface do usuário, use padrões de design, como eventos e coleções baseadas em notificação, como as encontradas em System.Collections.ObjectModel.

Design de objeto e membro (para bibliotecas a serem usadas em outras linguagens .NET)

Usar o atributo CLIEvent para expor eventos .NET

Construa um DelegateEvent com um tipo de delegado .NET específico que usa um objeto e EventArgs (em vez de um Event, que usa apenas o tipo FSharpHandler por padrão) para que os eventos sejam publicados da maneira familiar para outras linguagens do .NET.

type MyBadType() =
    let myEv = new Event<int>()

    [<CLIEvent>]
    member this.MyEvent = myEv.Publish

type MyEventArgs(x: int) =
    inherit System.EventArgs()
    member this.X = x

    /// A type in a component designed for use from other .NET languages
type MyGoodType() =
    let myEv = new DelegateEvent<EventHandler<MyEventArgs>>()

    [<CLIEvent>]
    member this.MyEvent = myEv.Publish

Expor operações assíncronas como métodos que retornam tarefas do .NET

As tarefas são usadas no .NET para representar cálculos assíncronos ativos. Em geral, as tarefas são menos composicionais do que os objetos de Async<T> F#, pois representam tarefas que já estão em execução e não podem ser combinadas de formas que realizem composição paralela ou que ocultem a propagação de sinais de cancelamento e outros parâmetros de contexto.

No entanto, apesar disso, os métodos que retornam Tarefas são a representação padrão da programação assíncrona no .NET.

/// A type in a component designed for use from other .NET languages
type MyType() =

    let compute (x: int): Async<int> = async { ... }

    member this.ComputeAsync(x) = compute x |> Async.StartAsTask

Com frequência, você também deseja aceitar um token de cancelamento explícito:

/// A type in a component designed for use from other .NET languages
type MyType() =
    let compute(x: int): Async<int> = async { ... }
    member this.ComputeAsTask(x, cancellationToken) = Async.StartAsTask(compute x, cancellationToken)

Usar tipos de representante do .NET em vez de tipos de função de F#

Aqui "Tipos de função F#" referem-se a tipos 'arrow', como int -> int.

Em vez disso:

member this.Transform(f: int->int) =
    ...

Faça isso:

member this.Transform(f: Func<int,int>) =
    ...

O tipo de função de F# aparece como class FSharpFunc<T,U> para outras linguagens .NET e é menos adequado para recursos de linguagem e ferramentas que entendem tipos de representante. Ao criar um método de ordem superior direcionado ao .NET Framework 3.5 ou superior, os representantes System.Func e System.Action são as APIs certas a publicar para permitir que os desenvolvedores do .NET as consumam com baixo atrito. (Ao direcionar o .NET Framework 2.0, os tipos de delegado definidos pelo sistema são mais limitados; considere usar tipos de delegado predefinidos, como System.Converter<T,U> ou definir um tipo de delegado específico.)

Por outro lado, os representantes do .NET não são naturais para bibliotecas voltadas a F# (confira a próxima seção sobre bibliotecas voltadas a F#). Como resultado, uma estratégia de implementação comum ao desenvolver métodos de ordem superior para bibliotecas .NET padrão é criar toda a implementação usando tipos de função F# e, em seguida, criar a API pública usando delegados como uma camada fina sobre a implementação real do F#.

Adote o padrão TryGetValue em vez de retornar valores de opção F# e prefira a sobrecarga de método em vez de usar valores de opção F# como argumentos.

Padrões comuns de uso do tipo de opção de F# em APIs são melhor implementados em APIs básicas do .NET usando técnicas de design .NET padrão. Em vez de retornar um valor de opção de F#, considere usar o tipo de retorno bool e um parâmetro de saída, como no padrão "TryGetValue". Além disso, em vez de usar valores de opção de F# como parâmetros, considere o uso da sobrecarga de método ou de argumentos opcionais.

member this.ReturnOption() = Some 3

member this.ReturnBoolAndOut(outVal: byref<int>) =
    outVal <- 3
    true

member this.ParamOption(x: int, y: int option) =
    match y with
    | Some y2 -> x + y2
    | None -> x

member this.ParamOverload(x: int) = x

member this.ParamOverload(x: int, y: int) = x + y

Usar os tipos de interface de coleção do .NET IEnumerable<T> e IDictionary<Key,Value> para parâmetros e valores de retorno

Evite o uso de tipos de coleção concretos, como matrizes .NET T[], tipos de F# list<T>, Map<Key,Value> e Set<T> e tipos de coleção concretos do .NET, como Dictionary<Key,Value>. As Diretrizes de Design da Biblioteca do .NET têm bons conselhos sobre quando usar vários tipos de coleção, como IEnumerable<T>. Alguns usos de matrizes (T[]) são aceitáveis em algumas circunstâncias, por motivos de desempenho. Observe especialmente que seq<T> é apenas o sinônimo F# para IEnumerable<T>e, portanto, seq é geralmente um tipo adequado para uma API .NET padrão.

Em vez de listas de F#:

member this.PrintNames(names: string list) =
    ...

Use sequências F#:

member this.PrintNames(names: seq<string>) =
    ...

Use o tipo de unidade como o único tipo de entrada de um método para definir um método de argumento zero ou como o único tipo de retorno para definir um método de retorno nulo

Evite outros usos do tipo de unidade. Estes são bons:

✔ member this.NoArguments() = 3

✔ member this.ReturnVoid(x: int) = ()

Isso é ruim:

member this.WrongUnit( x: unit, z: int) = ((), ())

Verificar se há valores nulos nos limites da API padrão do .NET

O código de implementação F# tende a ter menos valores nulos devido a padrões de design imutáveis e restrições ao uso de literais nulos para tipos F#. Outras linguagens .NET geralmente usam nulo como um valor com muito mais frequência. Por isso, o código F# que está expondo uma API .NET simples deve verificar parâmetros nulos na fronteira da API e impedir que esses valores fluam mais profundamente para o código de implementação F#. A função isNull ou a correspondência de padrões no padrão null pode ser usada.

let checkNonNull argName (arg: obj) =
    match arg with
    | null -> nullArg argName
    | _ -> ()

let checkNonNull' argName (arg: obj) =
    if isNull arg then nullArg argName
    else ()

A partir do F# 9, você pode aproveitar a nova sintaxe | null para fazer com que o compilador indique possíveis valores nulos e onde eles precisam de tratamento:

let checkNonNull argName (arg: obj | null) =
    match arg with
    | null -> nullArg argName
    | _ -> ()

let checkNonNull' argName (arg: obj | null) =
    if isNull arg then nullArg argName 
    else ()

No F# 9, o compilador emite um aviso quando detecta que um possível valor nulo não é tratado:

let printLineLength (s: string) =
    printfn "%i" s.Length

let readLineFromStream (sr: System.IO.StreamReader) =
    // `ReadLine` may return null here - when the stream is finished
    let line = sr.ReadLine()
    // nullness warning: The types 'string' and 'string | null'
    // do not have equivalent nullability
    printLineLength line

Esses avisos devem ser resolvidos usando o padrão nulo do F# na correspondência:

let printLineLength (s: string) =
    printfn "%i" s.Length

let readLineFromStream (sr: System.IO.StreamReader) =
    let line = sr.ReadLine()
    match line with
    | null -> ()
    | s -> printLineLength s

Evitar o uso de tuplas como valores de retorno

Em vez disso, prefira retornar um tipo nomeado que contém os dados agregados ou usar parâmetros externos para retornar vários valores. Embora haja tuplas e tuplas de struct no .NET (incluindo suporte à linguagem C# para tuplas de struct), na maioria das vezes, elas não fornecerão a API ideal e esperada para os desenvolvedores do .NET.

Evitar o uso de currying de parâmetros

Em vez disso, use as convenções de chamada do .NET Method(arg1,arg2,…,argN).

member this.TupledArguments(str, num) = String.replicate num str

Dica: se você estiver criando bibliotecas para uso com qualquer linguagem .NET, não há substituto para realmente fazer alguma programação experimental em C# e Visual Basic para garantir que suas bibliotecas "pareçam corretas" nessas linguagens. Você também pode usar ferramentas como o Refletor do .NET e o Navegador de Objetos do Visual Studio para garantir que as bibliotecas e sua documentação apareçam conforme o esperado para os desenvolvedores.

Apêndice

Exemplo de ponta a ponta de criação de código F# para uso por outras linguagens do .NET

Considere a seguinte classe:

open System

type Point1(angle,radius) =
    new() = Point1(angle=0.0, radius=0.0)
    member x.Angle = angle
    member x.Radius = radius
    member x.Stretch(l) = Point1(angle=x.Angle, radius=x.Radius * l)
    member x.Warp(f) = Point1(angle=f(x.Angle), radius=x.Radius)
    static member Circle(n) =
        [ for i in 1..n -> Point1(angle=2.0*Math.PI/float(n), radius=1.0) ]

O tipo F# inferido dessa classe é o seguinte:

type Point1 =
    new : unit -> Point1
    new : angle:double * radius:double -> Point1
    static member Circle : n:int -> Point1 list
    member Stretch : l:double -> Point1
    member Warp : f:(double -> double) -> Point1
    member Angle : double
    member Radius : double

Vamos dar uma olhada em como esse tipo F# aparece para um programador usando outra linguagem .NET. Por exemplo, a "assinatura" aproximada do C# é a seguinte:

// C# signature for the unadjusted Point1 class
public class Point1
{
    public Point1();

    public Point1(double angle, double radius);

    public static Microsoft.FSharp.Collections.List<Point1> Circle(int count);

    public Point1 Stretch(double factor);

    public Point1 Warp(Microsoft.FSharp.Core.FastFunc<double,double> transform);

    public double Angle { get; }

    public double Radius { get; }
}

Há alguns pontos importantes a serem notados sobre como F# representa construções aqui. Por exemplo:

  • Metadados como nomes de argumento foram preservados.

  • Métodos F# que recebem dois argumentos tornam-se métodos C# que recebem dois argumentos.

  • Funções e listas se tornam referências a tipos correspondentes na biblioteca F#.

O código a seguir mostra como ajustar esse código para levar essas coisas em conta.

namespace SuperDuperFSharpLibrary.Types

type RadialPoint(angle:double, radius:double) =

    /// Return a point at the origin
    new() = RadialPoint(angle=0.0, radius=0.0)

    /// The angle to the point, from the x-axis
    member x.Angle = angle

    /// The distance to the point, from the origin
    member x.Radius = radius

    /// Return a new point, with radius multiplied by the given factor
    member x.Stretch(factor) =
        RadialPoint(angle=angle, radius=radius * factor)

    /// Return a new point, with angle transformed by the function
    member x.Warp(transform:Func<_,_>) =
        RadialPoint(angle=transform.Invoke angle, radius=radius)

    /// Return a sequence of points describing an approximate circle using
    /// the given count of points
    static member Circle(count) =
        seq { for i in 1..count ->
                RadialPoint(angle=2.0*Math.PI/float(count), radius=1.0) }

O tipo F# inferido do código é o seguinte:

type RadialPoint =
    new : unit -> RadialPoint
    new : angle:double * radius:double -> RadialPoint
    static member Circle : count:int -> seq<RadialPoint>
    member Stretch : factor:double -> RadialPoint
    member Warp : transform:System.Func<double,double> -> RadialPoint
    member Angle : double
    member Radius : double

A assinatura do C# agora é a seguinte:

public class RadialPoint
{
    public RadialPoint();

    public RadialPoint(double angle, double radius);

    public static System.Collections.Generic.IEnumerable<RadialPoint> Circle(int count);

    public RadialPoint Stretch(double factor);

    public RadialPoint Warp(System.Func<double,double> transform);

    public double Angle { get; }

    public double Radius { get; }
}

As correções feitas para preparar esse tipo para uso numa biblioteca .NET padrão são as seguintes:

  • Vários nomes ajustados: Point1, n, le f tornaram-se RadialPoint, count, factore transform, respectivamente.

  • Usou um tipo de retorno de seq<RadialPoint> em vez de RadialPoint list alterando uma construção de lista usando [ ... ] para uma construção de sequência usando IEnumerable<RadialPoint>.

  • Usou o tipo de delegado .NET System.Func em vez de um tipo de função F#.

Isso torna muito mais agradável o consumo no código em C#.