Partilhar via


Diretrizes de design de componentes F#

Este documento é um conjunto de diretrizes de design de componentes para programação em F#, com base nas Diretrizes de Design de Componentes em F#, v14, Microsoft Research, e 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#. Muito obrigado à comunidade F# por suas contribuições e feedback útil 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 pelo código F# através dos limites do assembly.
  • Uma biblioteca destinada ao consumo por qualquer linguagem .NET através dos limites de 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 F#e, portanto, utilizam programação funcional e de objetos 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 .NET irá guiá-lo para a criação de um conjunto consistente de APIs que sejam fáceis de utilizar.

Orientações gerais

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

Conheça as diretrizes de design da biblioteca .NET

Independentemente do tipo de codificação F# que você está fazendo, é valioso ter um conhecimento prático do .NET Library Design Guidelines. 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 .NET fornecem orientação geral sobre nomenclatura, criação de classes e interfaces, design de membros (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 da documentação XML ao seu código

A documentação XML em APIs públicas garante que os usuários possam obter ótimos Intellisense e Quickinfo ao usar esses tipos e membros, além de habilitar a criação de arquivos de documentação para a biblioteca. Consulte a documentação XML sobre várias etiquetas xml que podem ser usadas para marcação adicional em 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 forma curta (/// comment) ou os comentários XML padrão (///<summary>comment</summary>).

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

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 toda a superfície pública da sua biblioteca e fornece uma separação clara 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 as alterações sejam feitas 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 se espera mais que mude significativamente.

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

Siga a orientação das Práticas recomendadas para usar strings no .NET quando o escopo do projeto o justificar. Em particular, declarar explicitamente intenção cultural na conversão e comparação de cordas (quando 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 expondo APIs públicas que se destinam a ser consumidas por desenvolvedores de F#. Há uma variedade de recomendações de design de biblioteca aplicáveis especificamente ao F#. Na ausência das recomendações específicas a seguir, as Diretrizes de Design da Biblioteca .NET são a orientação alternativa.

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 as construções do F#. Essas recomendações são especialmente destinadas a APIs que vão além dos limites de F#, compatíveis com os padrões idiomáticos da BCL do .NET e da maioria das bibliotecas.

Construção Caso Peça Exemplos Observações
Tipos de betão PascalCase Substantivo/adjetivo Lista, Duplo, Complexo Tipos concretos são estruturas, classes, enumerações, delegados, recordes 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
Tags do sindicato PascalCase Substantivo Alguns, Adicionar, Sucesso Não use um prefixo em APIs públicas. Opcionalmente, use um prefixo quando interno, como "type Teams = TAlpha | TBeta | TDelta".
Evento PascalCase Verbo ValorAlterado / ValorEmAlteração
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
Espaço de nomes PascalCase Microsoft.FSharp.Core Geralmente use <Organization>.<Technology>[.<Subnamespace>], embora abandone a organização se a tecnologia for independente da organização.
Parâmetros camelCase Substantivo tipoNome, transformar, intervalo
valores de let (internos) camelCase ou PascalCase Substantivo/verbo getValue, myTable
valores de let (externos) camelCase ou PascalCase Substantivo/verbo Lista.map, Datas.Hoje Os valores let-bound geralmente são públicos quando seguem padrões de design funcionais tradicionais. No entanto, geralmente use PascalCase quando o identificador pode ser usado por outras linguagens .NET.
Propriedade PascalCase Substantivo/adjetivo IsEndOfFile, BackColor As propriedades booleanas geralmente usam Is e Can e devem ser afirmativas, como em IsEndOfFile, não IsNotEndOfFile.

Evite abreviaturas

As diretrizes do .NET desencorajam o uso de abreviaturas (por exemplo, "use OnButtonClick em vez de OnBtnClick"). Abreviaturas comuns, como Async para "Assíncrono", são toleradas. Esta diretriz às vezes é ignorada para programação funcional; por exemplo, List.iter usa uma abreviatura para "iterar". Por esta razão, o uso de abreviaturas 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.

Evite colisões de nomes de invólucros

As diretrizes do .NET dizem que o invólucro sozinho não pode ser usado para desambiguar colisões de nome, uma vez que algumas linguagens de cliente (por exemplo, Visual Basic) não diferenciam maiúsculas de minúsculas.

Utilizar acrónimos sempre que adequado

Acrônimos como XML não são abreviaturas e são amplamente usados em bibliotecas .NET em formato sem maiúsculas (Xml). Só devem ser utilizadas siglas bem conhecidas e amplamente reconhecidas.

Use 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 para F#. Em particular, 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). Em ambos os casos, os nomes das funções agem como palavras-chave na linguagem.

Design de objeto, tipo e módulo

Use namespaces ou módulos para conter seus 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 =
    ...

ou

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:

  • Os 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 para qualquer módulo deve estar contido em um único arquivo
  • Os 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 outras linguagens .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 para um determinado membro não precisa necessariamente ser implementada nesse membro, mas a parte utilizável dessa funcionalidade deve sê-lo.

Usar classes para encapsular o estado mutável

Em F#, isso só precisa ser feito onde esse estado ainda não está encapsulado por outra construção de linguagem, como um fechamento, expressão de sequência ou 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 é preferível 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

Em preferência a:

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 os registros de funções não podem.

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 em FSharp.Core.

Use um módulo para agrupar funções para 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) fornecidas pelo FSharp.Core.dll.

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

Considere usar RequireQualifiedAccess e aplique cuidadosamente os 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 requerem 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 capacidade de evolução a longo prazo de uma biblioteca.

É altamente recomendável ter o atributo [<RequireQualifiedAccess>] para módulos personalizados que estendem aqueles fornecidos por FSharp.Core (como Seq, List, Array), pois esses módulos são predominantemente usados no código F# e [<RequireQualifiedAccess>] definidos neles; Mais geralmente, é desencorajado definir módulos personalizados sem o atributo, quando esse módulo faz sombra ou estende 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 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 contendo 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 colocará os nomes erf e erfc no escopo. Outro bom uso do [<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.

Considerar a definição de membros do operador em classes onde 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

Esta orientação corresponde à orientação geral do .NET para esses tipos. No entanto, isso 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 compatível com .NET para outros consumidores de linguagens .NET.

Por vezes, pode querer nomear algo num estilo para consumidores de F# (como um membro estático em minúsculas para que pareça ser uma função vinculada a um módulo), mas utilizar um estilo diferente para o nome quando este é compilado numa biblioteca. Você pode usar o atributo [<CompiledName>] para fornecer um estilo diferente para 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>], pode aplicar as convenções de nomenclatura .NET para os consumidores que não usam F# no assembly.

Use a sobrecarga de método para funções de membro, se isso fornecer 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 do que os tipos de argumentos.

Oculte as representações dos tipos de registro e união se o design desses tipos for suscetível de evoluir

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 design da biblioteca .NET. Em tempo de execução, o Common Language Runtime conhece a implementação comprometida que será usada durante toda a execução. No entanto, o código compilado por si só não capta dependências da representação concreta.

Evite 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 muitas vezes complexas e difíceis de alterar quando surgem novos requisitos. A implementação de herança ainda existe em F# para compatibilidade e casos raros em que é a melhor solução para um problema, mas técnicas alternativas devem ser consideradas nos seus programas em F# ao projetar para o polimorfismo, como a implementação de interfaces.

Assinaturas de funções e de membros

Use tuplas para valores de retorno quando estiver a devolver um pequeno número de vários valores não relacionados.

Aqui está um bom exemplo de uso de uma tupla em um tipo de retorno:

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

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

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

Se houver uma operação síncrona correspondente chamada Operation que retorna um T, a operação assíncrona deve 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 .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 Gerenciamento de Erros para saber mais sobre o uso apropriado de exceções, resultados e opções.

Membros da Extensão

Aplique cuidadosamente os membros da extensão F# nos componentes F#-to-F#

Os membros da extensão F# geralmente só devem ser usados para operações que estão no encerramento de operações intrínsecas associadas a um tipo na maioria de seus modos de uso. Um uso comum é fornecer APIs que são 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 é estranho com a herança, mas elegante com as uniões discriminadas.

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

Representar dados em estrutura de árvore com Uniões Discriminadas permite também beneficiar-se da exaustividade na correspondência de padrões.

Use [<RequireQualifiedAccess>] em tipos de união cujos nomes de caso não são suficientemente únicos

Pode encontrar-se num domínio em que o mesmo nome é o melhor nome para coisas diferentes, como casos de União Discriminada. Você pode usar [<RequireQualifiedAccess>] para desambiguar nomes de casos, a fim de evitar disparar erros confusos devido ao "shadowing" dependente da ordenação das instruções open.

Oculte as representações de uniões discriminadas para APIs compatíveis com binários se o design desses tipos for suscetível de evoluir

Os tipos de união dependem de formulários de correspondência de padrões F# para um modelo de programação sucinto. Como mencionado anteriormente, você deve evitar revelar representações de dados concretas se o design desses tipos provavelmente evoluir.

Por exemplo, a representação de um sindicato discriminado 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 sindicatos discriminados indiscriminadamente, poderá achar difícil fazer a versão da sua biblioteca sem quebrar o código do usuário. Em vez disso, considere revelar um ou mais padrões ativos para permitir a comparação de padrões sobre valores do seu tipo.

Os padrões ativos oferecem uma forma alternativa de realizar correspondência de padrões para os utilizadores de F#, evitando a exposição direta dos Tipos de União de F#.

Funções embutidas e restrições de membros

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

Restrições de membros aritméticos e 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 desta 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

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

Evite usar restrições de membro para simular classes de texto e digitação de pato

É possível simular "duck typing" usando restrições de membros em F#. No entanto, os membros que fazem uso disso não devem, em geral, ser usados em designs de bibliotecas F#-to-F#. Isso ocorre porque os 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 grande probabilidade de que o uso intensivo de restrições de membros dessa maneira possa resultar em tempos de compilação muito longos.

Definições do 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 nos mecanismos de pesquisa.

Como resultado, é melhor publicar sua funcionalidade como funções nomeadas e membros e, adicionalmente, expor os operadores para essa funcionalidade somente se os benefícios notacionais superarem a documentação e o custo cognitivo de tê-los.

Unidades de medida

Use cuidadosamente unidades de medida para maior segurança de tipo no código F#

Informações de digitação adicionais para unidades de medida são apagadas quando visualizadas por outros idiomas .NET. Lembre-se de que os componentes, ferramentas e mecanismos de reflexão do .NET tratarão tipos sem unidades. Por exemplo, os consumidores de C# verão float em vez de float<kg>.

Tipo de Abreviaturas

Use cuidadosamente abreviaturas de tipo para simplificar o código F#

Os componentes, as ferramentas e a reflexão do .NET não verão nomes abreviados para tipos. O uso significativo de abreviaturas 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 abreviaturas de tipo para tipos públicos cujos membros e propriedades devem ser intrinsecamente diferentes daqueles disponíveis no tipo a ser abreviado

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

Por exemplo, é tentador definir um multimapa 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 pontos 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 a partir de outras linguagens .NET

Ao projetar bibliotecas para uso de outras linguagens .NET, é importante aderir às .NET Library Design Guidelines. Neste documento, essas bibliotecas são designadas como bibliotecas .NET básicas, em oposição às bibliotecas destinadas ao F# que utilizam construções F# sem restrições. Projetar bibliotecas .NET vanilla significa fornecer APIs familiares e idiomáticas consistentes com o resto do .NET Framework minimizando o uso de construções específicas do F# na API pública. As regras são explicadas nas secções seguintes.

Design de namespace e tipo (para bibliotecas para uso de outras linguagens .NET)

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

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

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

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

Use namespaces, tipos e membros como a estrutura organizacional principal 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 armazenar código de implementação, tipos de utilitário e funções de utilitários.

Os tipos estáticos devem ser preferidos em relação aos módulos, pois permitem a evolução futura da API para usar sobrecarga e outros conceitos de design da API .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

Considere, em vez disso:

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

Use tipos de registo F# em APIs .NET básicas se o design dos tipos não evoluir

Os tipos de registo F# são compilados para 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. Evite também usar campos de registro mutáveis em APIs .NET vanilla, 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 seguinte código F# expõe a API pública a um consumidor 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 dos tipos de união F# nas APIs .NET padrão

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

Ao projetar uma API .NET vanilla, 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

Você também pode aumentar os tipos que usam uma representação de união internamente com membros para fornecer uma API voltada para .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)

Conceber GUI e outros componentes usando os padrões de design do framework

Há muitas estruturas diferentes disponíveis no .NET, como WinForms, WPF e ASP.NET. As 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 WPF para as classes que você está projetando. Para modelos em programação de interface do usuário, use padrões de design, como eventos e coleções baseadas em notificações, como os encontrados em System.Collections.ObjectModel.

Design de Objetos e Membros (para bibliotecas destinadas ao uso por outras linguagens .NET)

Use 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 .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 .NET

As tarefas são usadas no .NET para representar cálculos assíncronos ativos. As tarefas são, em geral, menos composicionais do que os objetos Async<T> F#, uma vez que representam tarefas "já em execução" e não podem ser compostas juntas de maneiras que executam composição paralela, ou que ocultam 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

Frequentemente, você também desejará 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 delegação .NET em vez de tipos de função F#

Aqui, "tipos de função F#" significam tipos de "seta" como int -> int.

Em vez disso:

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

Faça o seguinte:

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

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

Por outro lado, os delegados do .NET não são naturais para bibliotecas voltadas para F# (consulte a próxima Seção sobre bibliotecas voltadas para F#). Como resultado, uma estratégia comum no desenvolvimento de 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 utilizando delegados como uma camada fina de fachada sobre a implementação F# real.

Use o padrão TryGetValue em vez de retornar valores de opção F# e prefira sobrecarregar métodos em vez de aceitar valores de opção F# como argumentos.

Padrões comuns de uso para o tipo de opção F# em APIs são melhor implementados em APIs .NET vanilla usando técnicas de design .NET padrão. Em vez de retornar um valor de opção F#, considere usar o tipo de retorno booleano mais um parâmetro de saída como no padrão "TryGetValue". E, em vez de tomar valores de opção F# como parâmetros, considere usar sobrecarga de método ou 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

Utilize os tipos de interface de coleção .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 F# list<T>, Map<Key,Value> e Set<T>, e tipos de coleção concretos .NET, como Dictionary<Key,Value>. As Diretrizes de Design da Biblioteca .NET têm bons conselhos sobre quando usar vários tipos de coleção, como IEnumerable<T>. Algum uso de arrays (T[]) é aceitável em algumas circunstâncias, por motivos de desempenho. Note que seq<T> é apenas o alias F# para IEnumerable<T>e, portanto, seq é geralmente um tipo apropriado para uma API .NET padrão.

Em vez de listas 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 de vazio

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) = ((), ())

Verifique se há valores nulos nos limites da API .NET vanilla

O código de implementação F# tende a ter menos valores nulos, devido a padrões de design imutáveis e restrições no uso de literais nulos para tipos F#. Outras linguagens .NET geralmente usam null como um valor com muito mais frequência. Por isso, o código F# que está expondo uma API .NET padrão deve verificar se os parâmetros são nulos no limite ou fronteira da API e evitar que estes valores se propaguem mais profundamente para o código de implementação F#. A função isNull ou a correspondência de padrões do 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 ser manipulados:

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 ()

Em F# 9, o compilador emite um aviso quando deteta que um possível valor nulo não é manipulado:

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 abordados usando F# padrão nulo 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

Evite usar tuplas como valores de retorno

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

Evite o uso de currying de parâmetros

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

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

Dica: Se você estiver projetando bibliotecas para uso a partir de qualquer linguagem .NET, não há substituto para realmente fazer alguma programação experimental em C# e Visual Basic para garantir que suas bibliotecas "se sintam bem" a partir dessas linguagens. Você também pode usar ferramentas como o .NET Refletor e o Visual Studio Object Browser para garantir que as bibliotecas e sua documentação apareçam conforme esperado para os desenvolvedores.

Apêndice

Exemplo completo de criação de código 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 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

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 observados sobre como o F# representa construções aqui. Por exemplo:

  • Os metadados, como nomes de argumentos, foram preservados.

  • Os métodos F# que usam dois argumentos transformam-se em métodos C# que usam dois parâmetros.

  • Funções e listas tornam-se referências a tipos correspondentes na biblioteca 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 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 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 como parte de uma biblioteca .NET padrão são as seguintes:

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

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

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

Isso torna mais agradável consumir no código C#.