Compartilhar via


Diretrizes de design de componente do F#

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

Este documento pressupõe que você esteja familiarizado com a programação em F#. Agradecemos à comunidade de F# por suas contribuições e comentários úteis em diversas versões deste guia.

Visão geral

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

  • Uma camada no projeto de F# que contém os consumidores externos no 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 comuns ao tentar criar uma API que seja mais facilmente utilizável pelos desenvolvedores. A aplicação consciente das Diretrizes de Design de Biblioteca .NET permitirá a criação de um conjunto consistente de APIs que sejam agradáveis de consumir.

Diretrizes gerais

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

Saiba quais são as Diretrizes de Design de Biblioteca .NET

Independentemente do tipo de codificação em F#, é importante ter um conhecimento prático das Diretrizes de Design de Biblioteca .NET. A maioria dos outros programadores de F# e .NET estão familiarizados com essas diretrizes e esperam que o código .NET esteja em conformidade com elas.

As Diretrizes de Design de Biblioteca .NET fornecem orientação geral sobre nomenclatura, design de classes e interfaces, design de membro (propriedades, métodos, eventos etc.) e muito mais, e são uma primeira referência útil para uma variedade de diretrizes de design.

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

A documentação XML em APIs públicas garante que os usuários possam obter Intellisense e Quickinfo de qualidade ao usar esses tipos e membros e habilitar a criação de arquivos de documentação para a biblioteca. Confira a Documentação XML sobre diversas marca XML que podem ser usadas para marcação adicional nos comentários da documentação XML.

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

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

É possível usar os comentários XML de forma abreviada (/// comment) ou os comentários XML padrão (///<summary>comment</summary>).

Considere o uso de arquivos de assinatura explícita (.fsi) para bibliotecas estáveis e APIs de componentes

Ao usar arquivos de assinatura explícita em uma biblioteca em F#, é possível obter um breve resumo da API pública, o que ajuda a garantir que você conheça toda a superfície pública da biblioteca e fornece uma separação clara entre os detalhes da implementação interna e a documentação pública. Os arquivos de assinatura adicionam atrito à alteração da API pública, exigindo alterações nos arquivos de implementação e assinatura. Como resultado, os arquivos de assinatura normalmente só devem ser introduzidos quando uma API se solidificou e não é mais esperado que ela mude significativamente.

Siga as melhores práticas para o uso de 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 a F#

Esta seção apresenta recomendações para o desenvolvimento de bibliotecas públicas voltadas a F#, que expõem APIs públicas destinadas ao consumo por desenvolvedores de F#. Há uma variedade de recomendações de design de biblioteca aplicáveis especificamente a 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 de uso de maiúsculas e minúsculas do .NET

A tabela a seguir está em conformidade com as convenções de nomenclatura e uso de maiúsculas e minúsculas do .NET. Pequenas adições devem ser feitas para também incluir construtos em 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.

Constructo Caixa Parte Exemplos Observaçõ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 tipos sejam tradicionalmente escritos em minúsculas em OCaml, a F# adotou o esquema de nomenclatura do .NET para eles.
DLLs PascalCase Fabrikam.Core.dll
Marcas de união PascalCase Substantivo Some, Add, Success Não use prefixo em APIs públicas. Opcionalmente, use um prefixo quando interno, como "type Teams = TAlpha | Eta | Fulano".
Evento PascalCase Verbo ValueChanged/ValueChanging
Exceções PascalCase WebException O nome deve terminar com "Exceção".
Campo PascalCase Substantivo CurrentName
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, use PascalCase no geral quando o identificador puder ser usado em outras linguagens .NET.
Propriedade PascalCase Substantivo/adjetivo IsEndOfFile, BackColor Geralmente, as propriedades boolianas 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 a programação funcional, por exemplo, List.iter usa uma abreviação para "iterar". Por esse motivo, o uso de abreviações tende a ser mais tolerado na programação de F# para F#, mas ainda deve ser 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 minúsculas (Xml). Use apenas acrônimos conhecidos e amplamente reconhecidos.

Usar PascalCase para nomes de parâmetros genéricos

Use PascalCase para nomes de parâmetros genéricos em APIs públicas, inclusive para bibliotecas voltadas a F#. Em particular, use nomes como T, U, T1 e T2 para parâmetros genéricos arbitrários. No entanto, no caso das bibliotecas voltadas a F#, quando o uso de nomes específicos for adequado, use nomes como Key, Value e Arg (mas não TKey, por exemplo).

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

O camelCase é usado para funções públicas desenvolvidas para uso sem qualificação (por exemplo, invalidArg) e para as "funções de coleção padrão" (por exemplo, List.map). Em ambos os casos, os nomes das funções atuam como palavras-chave na linguagem.

Design de objeto, tipo e módulo

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

Cada arquivo de 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 =
    ...

ou

module Fabrikam.BasicOperationsAndTypes

type ObjectType1() =
    ...

type ObjectType2() =
    ...

module CommonOperations =
    ...

Veja as seguintes diferenças entre o uso de módulos e namespaces para organizar códigos no nível superior:

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

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

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

Ao trabalhar com objetos, é melhor garantir que uma funcionalidade consumível seja implementada na forma de 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 estados mutáveis

Em F#, isso só precisa ser feito quando o estado ainda não está encapsulado por outra construção de linguagem, como um encerramento, 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. Use essa opção como preferencial com relação a outras, 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
}

Interfaces são conceitos de primeira classe no .NET, e podem ser usadas para o que Functors normalmente são. Além disso, elas podem ser usadas para codificar tipos existenciais no programa, o que não é possível com registros de funções.

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 =
        ...

Ao incluir esse módulo, siga as convenções de nomenclatura padrão para funções encontradas em FSharp.Core.

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

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

Da mesma forma, uma biblioteca de estatísticas pode incluir um módulo com funções erf e erfc, que foi projetado para ser aberto de maneira explícita ou automática.

Considere usar RequireQualifiedAccess e aplique cuidadosamente os atributos AutoOpen

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

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

É altamente sugerido ter o atributo [<RequireQualifiedAccess>] para módulos personalizados que estendem aqueles fornecidos por FSharp.Core (como Seq, List e Array), pois esses módulos são usados predominantemente no código F# e têm [<RequireQualifiedAccess>] definido neles; em geral, é desencorajado definir módulos personalizados sem o atributo, quando esses módulos fazem sombra ou estendem outros módulos que têm o atributo.

A adição do 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 deve conter um module MathsHeaven.Statistics.Operators com as 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 do atributo [<AutoOpen>] resulta em namespaces poluídos, por isso, ele 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 operadores em classes em que o uso de operadores conhecidos é apropriado

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

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

Esta orientação corresponde à diretriz geral do .NET para esses tipos. No entanto, ela pode ser ainda mais importante na codificação em F#, pois permite que esses tipos sejam usados com funções e métodos de F# com restrições de membro, como List.sumBy.

Considere usar CompiledName para fornecer um nome compatível com o .NET para outros consumidores da linguagem .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. É possível usar o atributo [<CompiledName>] para fornecer um estilo diferente para códigos que não estão em F# e que consomem 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

Com [<CompiledName>], é possível usar convenções de nomenclatura do .NET para consumidores do assembly que não são 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 opções ou argumentos diferentes.

type Logger() =

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

Em F#, é mais comum sobrecarregar o número de argumentos em vez de os 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 pública externa do design de biblioteca .NET. No runtime, o Common Language Runtime conhece a implementação confirmada que será usada durante a execução. No entanto, o código compilado não obtêm 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 são frequentemente complexas e difíceis de mudar diante de novos requisitos. 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.

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

Se houver uma operação síncrona correspondente chamada Operation que retorna 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 a fim de fornecer o modelo de programação assíncrona em 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

Confira Gerenciamento de erros para aprender sobre o uso apropriado de exceções, resultados e opções.

Membros de 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 a F# para diversos tipos .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

É comum haver um domínio em que o mesmo nome é ideal para coisas diferentes, como nos 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

Ocultar as representações de uniões discriminadas para APIs compatíveis com binários quando houver probabilidade de aprimoramento do design desses tipos

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, evite revelar representações de dados concretas quando houver probabilidade de aprimoramento do design desses tipos.

Por exemplo, a representação de uma união discriminada pode ser ocultada usando uma declaração privada ou interna ou 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

Defina algoritmos numéricos genéricos usando funções inline com restrições de membros 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

Esta função tem o seguinte tipo:

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

Esta é 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 porque os designs de biblioteca baseados em restrições implícitas desconhecidas ou não padrão tendem a tornar o código do usuário inflexível e vinculá-lo a um padrão de estrutura específico.

Além disso, é possível que o uso excessivo de restrições de membro dessa maneira resulte em tempos de compilação muito longos.

Definições de operador

Evitar definir operadores simbólicos personalizados

Os operadores personalizados são essenciais em algumas situações e são dispositivos de notação altamente úteis em um grande número de códigos de implementação. Para os novos usuários de uma biblioteca, é geralmente mais fácil usar as funções nomeadas. Além disso, os operadores simbólicos personalizados podem ser difíceis de documentar, e os usuários costumam ter dificuldade ao procurar por ajuda sobre eles, devido às limitações existentes no IDE e nos 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 unidades de medida com cuidado para aumentar a segurança de tipo no código em F#

As informações de digitação adicionais de unidades de medida são apagadas em exibições de outras linguagens .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 abreviações de tipo com cuidado para simplificar o código em F#

Os componentes, as ferramentas e a reflexão do .NET não verão nomes abreviados para tipos. O uso excessivo de abreviações de tipo também pode fazer um domínio parecer mais complexo do que realmente é, o que pode confundir os consumidores.

Evite abreviações de tipos públicos com membros e propriedades que 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, você pode querer definir um multimapa como um caso especial de um mapa em F#:

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 iguais às operações em um mapa. Por exemplo, é razoável que o operador de pesquisa map[key] retorne a lista vazia caso a chave não esteja no dicionário em vez de gerar uma exceção.

Diretrizes para bibliotecas sobre o uso de outras linguagens .NET

Ao desenvolver bibliotecas para uso por outras linguagens .NET, é importante aderir às Diretrizes de Design de Biblioteca .NET. Neste documento, essas bibliotecas são rotuladas como bibliotecas .NET básicas, em oposição às bibliotecas voltadas a F#, que usam constructos em F# sem restrições. Desenvolver bibliotecas .NET básicas significa fornecer APIs familiares e idiomáticas consistentes com o restante do .NET Framework, 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

Dê atenção especial ao uso de nomes abreviados e às diretrizes de uso de maiúsculas e minúsculas do .NET.

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

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

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

Todos os arquivos com funcionalidades públicas devem começar com uma declaração namespace e as únicas entidades voltadas ao público em namespaces devem ser tipos. Não use módulos de F#.

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

Prefira tipos estáticos a módulos, pois eles permitem o aprimoramento futuro da API para uso da sobrecarga e de outros conceitos de design da API do .NET que não podem ser usados nos módulos de 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

Considere esta alternativa:

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 de F# são compilados em uma classe .NET simples. Eles são adequados 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. Também evite usar campos de registro mutáveis em APIs básicas do .NET, pois elas expõem um campo público. Sempre considere se uma classe forneceria uma opção mais flexível para o aprimoramento futuro da API.

Por exemplo, o seguinte código em F# expõe a API pública a um consumidor de 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 usados internamente em componentes e bibliotecas.

Ao projetar uma API básica do .NET, 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)

Projetar a 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. As convenções de nomenclatura e design de cada uma delas devem ser usadas ao projetar componentes para uso nessas estruturas. Por exemplo, para programação em WPF, adote padrões de design de WPF para as classes que serão projetadas. Para modelos na programação da interface do usuário, use padrões de design, por exemplo, 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 representante .NET específico que receba um objeto e EventArgs (em vez de um Event, que simplesmente usa o tipo FSharpHandler por padrão) para que os eventos sejam publicados de maneira familiar para outras linguagens .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 computações assíncronas ativas. Em geral, elas são menos composicionais que os objetos Async<T> em F#, uma vez que representam tarefas "já em execução" e não podem ser colocadas juntas de maneira a realizar a composição paralela ou ocultar a propagação de sinais de cancelamento e outros parâmetros contextuais.

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

Recomenda-se também aceitar com frequência 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 de F#" significam tipos de "seta", como int -> int.

Em vez do seguinte:

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

Faça o seguinte:

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 focar no .NET Framework 2.0, os tipos de representantes definidos pelo sistema são mais limitados, por isso, considere usar tipos de representantes predefinidos, como System.Converter<T,U>, ou definir um tipo 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 básicas é criar toda a implementação usando tipos de função de F# e, em seguida, criar a API pública usando representantes como uma leve fachada sobre a implementação real em F#.

Use o padrão TryGetValue em vez de retornar valores de opção de F# e prefira a sobrecarga de método ao uso de valores de opção de 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 de Biblioteca .NET têm boas recomendações sobre quando usar diversos tipos de coleção, como IEnumerable<T>. Algum uso de matrizes (T[]) é aceitável em algumas circunstâncias, por motivos de desempenho. Observe especialmente que seq<T> é apenas o alias de F# para IEnumerable<T>, portanto, seq geralmente é um tipo apropriado para uma API básica do .NET.

Em vez de listas de F#:

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

Use sequências de F#:

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

Usar 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 usos:

✔ member this.NoArguments() = 3

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

Estes são usos ruins:

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

Verifique se há valores nulos nos limites da API básica do .NET

O código de implementação em F# tende a ter menos valores nulos, devido a padrões de design imutáveis e restrições de uso de literais nulos para tipos de F#. Outras linguagens .NET geralmente usam NULL como valor com muito mais frequência. Devido a isso, o código em F# que está expondo uma API básica do .NET deve verificar se há NULL nos parâmetros no limite da API e impedir que esses valores fluam mais profundamente para o código de implementação em F#. É possível usar a função isNull ou a correspondência de padrões no padrão null.

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

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

Evitar o uso de tuplas como valores de retorno

Em vez disso, prefira retornar um tipo nomeado que contenha os dados agregados ou usar parâmetros de saída para retornar diversos 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: para desenvolver bibliotecas a serem usadas por qualquer linguagem .NET, não há outra opção além da aplicação de programações experimentais em C# e em Visual Basic para garantir que elas "pareçam corretas" nessas linguagens. Também é possível usar ferramentas como o .NET Reflector e o Visual Studio Object Browser para garantir que as bibliotecas e a documentação associada apareçam conforme o esperado para os desenvolvedores.

Apêndice

Exemplo completo de design de códigos em F# para uso por outras linguagens .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 de F# inferido desta 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

Vejamos como esse tipo de F# aparece para um programador que usa outra linguagem .NET. Por exemplo, a "assinatura" aproximada de 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 observados sobre como a linguagem F# representa constructos aqui. Por exemplo:

  • Metadados como os nomes de argumentos foram preservados.

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

  • As funções e listas tornam-se referências a tipos correspondentes na biblioteca de F#.

O código a seguir mostra como ajustar esse código para levar essas coisas em consideração.

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 de 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 de 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; }
}

Veja as seguintes correções feitas para preparar esse tipo para uso como parte de uma biblioteca .NET básica:

  • Diversos nomes foram ajustados: Point1, n, l e f agora são RadialPoint, count, factor e transform, respectivamente.

  • Um tipo de retorno seq<RadialPoint> em vez de RadialPoint list foi usado, alterando uma construção de lista que usa [ ... ] para uma construção de sequência que usa IEnumerable<RadialPoint>.

  • O tipo de representante do .NET System.Func foi usado, em vez de um tipo de função de F#.

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