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.

Descriçã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 através de um repositório de pacotes, como o NuGet.

As técnicas descritas neste artigo seguem os Cinco princípios de um bom código F# e, portanto, utilizam a 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 orientará você para a criação de um conjunto consistente de APIs que são agradáveis de consumir.

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 das Diretrizes de Design da Biblioteca .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 .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 marcas 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 as práticas recomendadas para usar cadeias de caracteres na orientação do .NET quando o escopo do projeto o justificar. Em particular, declarar explicitamente a 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 as diretrizes de fallback.

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 também incluir construções F#. Essas recomendações são especialmente destinadas a APIs que ultrapassam os limites de F# para F#, ajustando-se a expressões idiomáticas da BCL do .NET e da maioria das bibliotecas.

Construção Incidente Parte Exemplos Notas
Tipos de betão PascalCase Substantivo/adjetivo Lista, Duplo, Complexo Tipos concretos são structs, classes, enumerações, delegados, registros e sindicatos. 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 da união PascalCase Substantivo Alguns, Acrescentar, 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 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
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 typeName, transform, range
valores de let (internos) camelCase ou PascalCase Substantivo/verbo getValue, myTable
valores de let (externos) camelCase ou PascalCase Substantivo/verbo List.map, Dates.Today 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 de outras linguagens .NET.
Property 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, então 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 consumível dessa funcionalidade deve ser.

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 por 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 [<RequireQualifiedAccess>] atributo 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 Microsoft.FSharp.Collections.List módulo 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.

É fortemente sugerido ter o [<RequireQualifiedAccess>] atributo 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 definidos [<RequireQualifiedAccess>] neles, mais geralmente, é desencorajado definir módulos personalizados sem o atributo, quando tal módulo sombreia ou estenda outros módulos que tenham o atributo.

Adicionar o [<AutoOpen>] atributo a um módulo significa que o módulo será aberto quando o namespace que contém for aberto. O [<AutoOpen>] atributo 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 funções module MathsHeaven.Statistics.Operators contendo erf e erfc. É razoável marcar este módulo como [<AutoOpen>]. Isso significa open MathsHeaven.Statistics que também abrirá este módulo e trará os nomes erf e erfc para o escopo. Outro bom uso é para módulos que contêm métodos de [<AutoOpen>] extensão.

O uso excessivo de leva a namespaces poluídos [<AutoOpen>] , e o atributo deve ser usado com cuidado. Para bibliotecas específicas em domínios específicos, o uso criterioso pode [<AutoOpen>] 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 arquivo . Nome amigável para NET para outros consumidores de idiomas .NET

Às vezes, você pode querer nomear algo em um estilo para consumidores de F# (como um membro estático em minúsculas para que pareça como se fosse uma função ligada a módulo), mas tenha um estilo diferente para o nome quando ele é compilado em um assembly. Você pode usar o [<CompiledName>] atributo 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

[<CompiledName>]Usando o , você pode usar convenções de nomenclatura .NET para consumidores não F# do 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 DateTime valores 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 buscadas em seus programas F# ao projetar para polimorfismo, como implementação de interface.

Assinaturas de funções e membros

Use tuplas para valores de retorno ao retornar 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 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, então 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 de 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 semelhantes a árvores com Uniões Discriminadas também permite que você se beneficie da exaustividade na correspondência de padrões.

Utilização [<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 para evitar desencadear erros confusos devido ao sombreamento dependente da ordem das open instruções

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 correspondência de padrões sobre valores do seu tipo.

Os padrões ativos fornecem uma maneira alternativa de fornecer aos consumidores de F# correspondência de padrões, evitando expor diretamente os 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 aritméticas de membros e restrições de comparação de F# são um padrão para programação de 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 membro 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 boa chance de que o uso pesado 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, as ferramentas e a reflexão do .NET verão tipos-sans-unidades. Por exemplo, os consumidores de C# verão float em vez de float<kg>.

Abreviaturas dos tipos

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 maiúsculas e minúsculas (ou, quando o desempenho for essencial, considere usar um tipo struct para encapsular 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 map[key] de pesquisa retorne 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 outros idiomas .NET

Ao projetar bibliotecas para uso de outras linguagens .NET, é importante aderir às Diretrizes de Design da Biblioteca .NET. Neste documento, essas bibliotecas são rotuladas como bibliotecas .NET baunilha, em oposição às bibliotecas voltadas para F# que usam 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 namespace declaração, 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 registro F# em APIs .NET vanilla se o design dos tipos não evoluir

Os tipos de registro F# compilam para uma classe .NET simples. Eles são adequados para alguns tipos simples e estáveis em APIs. Considere usar os [<NoEquality>] atributos 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 de tipos de união F# em APIs .NET vanilla

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 sindical internamente com os membros para fornecer um . 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)

GUI de design 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 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 na programação da interface do usuário, use padrões de design, como eventos e coleções baseadas em notificações, como os encontrados no System.Collections.ObjectModel.

Design de objeto e membro (para bibliotecas para uso de 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 apenas usa o FSharpHandler tipo 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 F# Async<T> , 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# aparece como class FSharpFunc<T,U> para outras linguagens .NET e é menos adequado para recursos de linguagem e ferramentas que entendem tipos delegados. Ao criar um método de ordem superior direcionado ao .NET Framework 3.5 ou superior, os System.Func e System.Action delegados são as APIs certas a serem publicadas para permitir que os desenvolvedores do .NET consumam essas APIs de maneira de baixo atrito. (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 de implementação comum ao desenvolver métodos de ordem superior para bibliotecas .NET vanilla é criar toda a implementação usando tipos de função F# e, em seguida, criar a API pública usando delegados como uma fachada fina sobre a implementação F# real.

Use o padrão TryGetValue em vez de retornar valores de opção F# e prefira sobrecarga de método a tomar 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 bool mais um parâmetro out 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

Use 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 de concreto, como matrizes T[].NET, tipos list<T>Map<Key,Value> F# e Set<T>, e tipos de coleção de concreto .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. Observe especialmente que seq<T> é apenas o alias F# para IEnumerable<T>, e, portanto, seq é muitas vezes um tipo apropriado para uma API .NET vanilla.

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 baunilha deve verificar parâmetros para nulo no limite da API e impedir que esses valores fluam mais profundamente para o código de implementação F#. A isNull função ou padrão correspondente no null padrão 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 ()

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 existam tuplas e tuplas struct no .NET (incluindo suporte à linguagem C# para tuplas struct), na maioria das vezes elas não fornecem a API ideal e esperada para desenvolvedores .NET.

Evite o uso de currying de parâmetros

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

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.

Anexo

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 tornam-se métodos C# que usam dois argumentos.

  • 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 baunilha são as seguintes:

  • Ajustou vários nomes: Point1, n, l, e f tornou-se RadialPoint, count, factor, e transform, respectivamente.

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

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

Isso torna muito melhor consumir em código C#.