Compartir vía


Instrucciones de diseño de componentes de F#

Este documento es un conjunto de directrices de diseño de componentes para la programación de F#, basadas en las Directrices de diseño de componentes de F#, v14, Microsoft Research y una versión que se ha mantenido y mantenido originalmente por F# Software Foundation.

En este documento se da por supuesto que estás familiarizado con la programación F#. Muchas gracias a la comunidad de F# por sus contribuciones y comentarios útiles sobre varias versiones de esta guía.

Información general

En este documento se examinan algunos de los problemas relacionados con el diseño y la codificación de componentes de F#. Un componente puede significar cualquiera de lo siguiente:

  • Una capa del proyecto de F# que tiene consumidores externos dentro de ese proyecto.
  • Una biblioteca diseñada para su consumo por código de F# a través de los límites del ensamblado.
  • Una biblioteca diseñada para su consumo por cualquier lenguaje .NET a través de los límites del ensamblado.
  • Una biblioteca destinada a la distribución a través de un repositorio de paquetes, como NuGet.

Las técnicas descritas en este artículo siguen los cinco principios del buen código de F# y, por tanto, utilizan la programación funcional y de objetos según corresponda.

Independientemente de la metodología, el diseñador de componentes y bibliotecas se enfrenta a una serie de problemas prácticos y prosaicos al intentar crear una API que los desarrolladores puedan usar más fácilmente. La aplicación consciente de las directrices de diseño de la biblioteca .NET le ayudará a crear un conjunto coherente de API que sean agradables de consumir.

Directrices generales

Hay algunas directrices universales que se aplican a las bibliotecas de F#, independientemente del público previsto para la biblioteca.

Más información sobre las directrices de diseño de la biblioteca de .NET

Independientemente del tipo de codificación de F# que esté haciendo, es valioso tener un conocimiento práctico de las directrices de diseño de la biblioteca .NET. La mayoría de los demás programadores de F# y .NET estarán familiarizados con estas directrices y esperan que el código de .NET se ajuste a ellas.

Las directrices de diseño de la biblioteca .NET proporcionan instrucciones generales sobre nomenclatura, diseño de clases e interfaces, diseño de miembros (propiedades, métodos, eventos, etc.) y mucho más, y son un primer punto de referencia útil para una variedad de instrucciones de diseño.

Adición de comentarios de documentación XML a tu código

La documentación XML sobre las API públicas garantiza que los usuarios puedan obtener excelentes IntelliSense e Quickinfo al usar estos tipos y miembros, y habilitar la creación de archivos de documentación para la biblioteca. Consulta la documentación XML sobre varias etiquetas xml que se pueden usar para el marcado adicional dentro de los comentarios xmldoc.

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

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

Puedes usar los comentarios XML de formato corto (/// comment) o los comentarios XML estándar (///<summary>comment</summary>).

Considera la posibilidad de usar archivos de firma explícitos (.fsi) para api de componentes y bibliotecas estables

El uso de archivos de firmas explícitas en una biblioteca de F# proporciona un resumen sucinto de la API pública, que ayuda a garantizar que conoce la superficie pública completa de la biblioteca y proporciona una separación limpia entre la documentación pública y los detalles de implementación internos. Los archivos de firma agregan fricción al cambio de la API pública, ya que requieren que se realicen cambios en los archivos de implementación y firma. Como resultado, los archivos de firma normalmente solo se deben introducir cuando una API se ha solidificado y ya no se espera que cambie significativamente.

Siga los procedimientos recomendados para usar cadenas en .NET.

Siga los procedimientos recomendados para usar cadenas en instrucciones de .NET cuando el ámbito del proyecto lo garantiza. En concreto, indica explícitamente intención cultural en la conversión y comparación de cadenas (si procede).

Directrices para bibliotecas orientadas a F#

En esta sección se presentan recomendaciones para desarrollar bibliotecas públicas de F#; es decir, las bibliotecas que exponen las API públicas que están diseñadas para ser consumidas por los desarrolladores de F#. Hay una variedad de recomendaciones de diseño de biblioteca aplicables específicamente a F#. En ausencia de las recomendaciones específicas siguientes, las directrices de diseño de la biblioteca de .NET son las instrucciones de reserva.

Convenciones de nomenclatura

Uso de convenciones de nomenclatura y mayúsculas de .NET

En la tabla siguiente se siguen las convenciones de nomenclatura y mayúsculas de .NET. También hay pequeñas adiciones para incluir construcciones de F#. Esas recomendaciones están especialmente pensadas para las API que van más allá de los límites de F#a F#, que se ajustan a las expresiones de la BCL de .NET y la mayoría de las bibliotecas.

Construcción Caso Parte Ejemplos Notas
Tipos concretos PascalCase Sustantivo/ adjetivo List, Double, Complex Los tipos concretos son estructuras, clases, enumeraciones, delegados, registros y uniones. Aunque los nombres de tipo tradicionalmente están en minúsculas en OCaml, F# ha adoptado el esquema de nomenclatura de .NET para los tipos.
DLL PascalCase Fabrikam.Core.dll
Etiquetas de unión PascalCase Nombre Algunos, Agregar, Correcto No use un prefijo en las API públicas. Opcionalmente, use un prefijo cuando sea interno, como "tipo Teams = Alpha | Beta | Delta".
Evento PascalCase Verbo ValueChanged/ValueChanging
Excepciones PascalCase WebException El nombre debe terminar con «Exception».
Campo PascalCase Nombre CurrentName
Tipos de interfaz PascalCase Sustantivo/ adjetivo IDisposable La dirección URL debe comenzar por «I».
Método PascalCase Verbo ToString
Espacio de nombres PascalCase Microsoft.FSharp.Core Por lo general, use <Organization>.<Technology>[.<Subnamespace>], aunque quite la organización si la tecnología es independiente de la organización.
Parámetros camelCase Nombre typeName, transform, range
let values (interno) camelCase o PascalCase Sustantivo/ verbo getValue, myTable
let values (externo) camelCase o PascalCase Sustantivo/verbo List.map, Dates.Today Los valores enlazados a let suelen ser públicos cuando se siguen los patrones de diseño funcional tradicionales. Sin embargo, por lo general, use PascalCase cuando el identificador se pueda usar desde otros lenguajes de .NET.
Propiedad PascalCase Sustantivo/ adjetivo IsEndOfFile, BackColor Las propiedades booleanas suelen usar Is y Can y deben ser afirmativas, como en IsEndOfFile, no IsNotEndOfFile.

Evitar abreviaturas

Las directrices de .NET desalientan el uso de abreviaturas (por ejemplo, «use OnButtonClick en lugar de OnBtnClick«). Las abreviaturas comunes, como Async «Asincrónica», se toleran. Esta guía a veces se omite para la programación funcional; por ejemplo, List.iter usa una abreviatura de «iteración». Por este motivo, el uso de abreviaturas tiende a tolerarse hasta un mayor grado en la programación F#-to-F#, pero por lo general debe evitarse en el diseño de componentes públicos.

Evitar conflictos de nombres de mayúsculas y minúsculas

Las instrucciones de .NET dicen que no se puede usar solo el uso de mayúsculas y minúsculas para la desambiguación de los conflictos de nombres, ya que algunos lenguajes de cliente (por ejemplo, Visual Basic) no distinguen mayúsculas de minúsculas.

Utilizar acrónimos cuando sea apropiado

Los acrónimos como XML no son abreviaturas y se usan ampliamente en las bibliotecas de .NET en forma no localizada (Xml). Solo se deben usar acrónimos conocidos y ampliamente reconocidos.

Uso de PascalCase para nombres de parámetro genéricos

Use PascalCase para los nombres de parámetro genérico en las API públicas, incluidas las bibliotecas orientadas a F#. En concreto, use nombres como T, U, T1, T2 para parámetros genéricos arbitrarios y cuando los nombres específicos tengan sentido, para las bibliotecas orientadas a F#, use nombres como Key, Value, Arg (pero no por ejemplo, TKey).

Usar PascalCase o camelCase para funciones y valores públicos en módulos de F#

camelCase se usa para funciones públicas diseñadas para usarse sin calificar (por ejemplo, invalidArg) y para las «funciones de colección estándar» (por ejemplo, List.map). En ambos casos, los nombres de función actúan como palabras clave en el lenguaje.

Diseño de objetos, tipos y módulos

Uso de espacios de nombres o módulos para contener los tipos y módulos

Cada archivo de F# de un componente debe comenzar con una declaración de espacio de nombres o una declaración de módulo.

namespace Fabrikam.BasicOperationsAndTypes

type ObjectType1() =
    ...

type ObjectType2() =
     ...

module CommonOperations =
    ...

o

module Fabrikam.BasicOperationsAndTypes

type ObjectType1() =
    ...

type ObjectType2() =
    ...

module CommonOperations =
    ...

Las diferencias entre el uso de módulos y espacios de nombres para organizar el código en el nivel superior son las siguientes:

  • Los espacios de nombres pueden abarcar diversos archivos
  • Los espacios de nombres no pueden contener funciones de F# a menos que estén dentro de un módulo interno
  • El código de cualquier módulo determinado debe estar incluido en un único archivo
  • Los módulos de nivel superior pueden contener funciones de F# sin necesidad de un módulo interno

La elección entre un espacio de nombres de nivel superior o un módulo afecta al formato compilado del código y, por tanto, afectará a la vista de otros lenguajes .NET si la API finalmente se consumirá fuera del código de F#.

Usar métodos y propiedades para operaciones intrínsecas a tipos de objeto

Al trabajar con objetos, es mejor asegurarse de que la funcionalidad consumible se implementa como métodos y propiedades en ese 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) = ...

La mayor parte de la funcionalidad de un miembro determinado no debe implementarse necesariamente en ese miembro, pero debe ser la parte consumible de esa funcionalidad.

Uso de clases para encapsular el estado mutable

En F#, esto solo debe realizarse cuando ese estado aún no esté encapsulado por otra construcción de lenguaje, como un cierre, una expresión de secuencia o un cálculo asincrónico.

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

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

Use tipos de interfaz para representar un conjunto de operaciones. Esto es preferible a otras opciones, como tuplas de funciones o registros de funciones.

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

En preferencia a:

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

Las interfaces son conceptos de primera clase en .NET, que puede usar para lograr lo que los Functors normalmente le darían. Además, se pueden usar para codificar tipos existenciales en el programa, que los registros de funciones no pueden.

Uso de un módulo para agrupar funciones que actúan en colecciones

Al definir un tipo de colección, considere la posibilidad de proporcionar un conjunto estándar de operaciones como CollectionType.map y CollectionType.iter) para los nuevos tipos de colección.

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

Si incluye este módulo, siga las convenciones de nomenclatura estándar para las funciones que se encuentran en FSharp.Core.

Uso de un módulo para agrupar funciones para funciones comunes y canónicas, especialmente en bibliotecas matemáticas y DSL

Por ejemplo, Microsoft.FSharp.Core.Operators es una colección abierta automáticamente de funciones de nivel superior (como abs y sin) proporcionadas por FSharp.Core.dll.

Del mismo modo, una biblioteca de estadísticas puede incluir un módulo con funciones erf y erfc, donde este módulo está diseñado para abrirse de forma explícita o automática.

Considere la posibilidad de usar RequireQualifiedAccess y aplicar cuidadosamente los atributos AutoOpen

Agregar el [<RequireQualifiedAccess>] atributo a un módulo indica que es posible que el módulo no se abra y que las referencias a los elementos del módulo requieran acceso explícito calificado. Por ejemplo, el módulo Microsoft.FSharp.Collections.List tiene este atributo.

Esto resulta útil cuando las funciones y los valores del módulo tienen nombres que probablemente entren en conflicto con los nombres de otros módulos. Requerir acceso cualificado puede aumentar considerablemente la capacidad de mantenimiento a largo plazo y la evolución de una biblioteca.

Se recomienda tener el atributo [<RequireQualifiedAccess>] para los módulos personalizados que amplían los proporcionados por FSharp.Core (como Seq, List, Array), ya que esos módulos se usan prevalentemente en el código F# y tienen [<RequireQualifiedAccess>] definido en ellos. En general, no se recomienda definir módulos personalizados que carezcan del atributo, cuando estos módulos sombrean o extienden otros módulos que tienen el atributo.

Agregar el [<AutoOpen>] atributo a un módulo significa que el módulo se abrirá cuando se abra el espacio de nombres contenedor. El [<AutoOpen>] atributo también se puede aplicar a un ensamblado para indicar un módulo que se abre automáticamente cuando se hace referencia al ensamblado.

Por ejemplo, una biblioteca de estadísticas MathsHeaven.Statistics podría contener un module MathsHeaven.Statistics.Operators con las funciones erf y erfc. Es razonable marcar este módulo como [<AutoOpen>]. Esto significa open MathsHeaven.Statistics que también abrirá este módulo y pondrá los nombres erf y erfc en el ámbito. Otro buen uso de [<AutoOpen>] es para los módulos que contienen métodos de extensión.

El uso excesivo de conduce a espacios de [<AutoOpen>] nombres contaminados y el atributo debe usarse con cuidado. En el caso de bibliotecas específicas en dominios específicos, el uso prudente de [<AutoOpen>] puede dar lugar a una mejor facilidad de uso.

Considere la posibilidad de definir miembros del operador en clases en las que el uso de operadores conocidos es adecuado

A veces, las clases se usan para modelar construcciones matemáticas como Vectores. Cuando el dominio que se modela tiene operadores conocidos, definirlos como miembros intrínsecos a la clase es ú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 guía corresponde a las instrucciones generales de .NET para estos tipos. Sin embargo, también puede ser importante en la codificación de F#, ya que esto permite usar estos tipos junto con funciones y métodos de F# con restricciones de miembro, como List.sumBy.

Considere la posibilidad de usar CompiledName para proporcionar un . Nombre descriptivo de NET para otros consumidores de lenguaje .NET

A veces, es posible que desee asignar un nombre a algo en un estilo para los consumidores de F# (por ejemplo, un miembro estático en minúsculas para que aparezca como si fuera una función enlazada a módulo), pero tener un estilo diferente para el nombre cuando se compila en un ensamblado. Puedes usar el [<CompiledName>] atributo para proporcionar un estilo diferente para el código que no es de F# que consume el ensamblado.

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

Mediante [<CompiledName>], puedes usar convenciones de nomenclatura de .NET para consumidores que no son de F# del ensamblado.

Usa la sobrecarga de métodos para las funciones miembro; si hacerlo proporciona una API más sencilla

La sobrecarga de métodos es una herramienta eficaz para simplificar una API que puede necesitar realizar una funcionalidad similar, pero con diferentes opciones o argumentos.

type Logger() =

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

En F#, es más común sobrecargar el número de argumentos en lugar de los tipos de argumentos.

Ocultar las representaciones de los tipos de registro y unión si es probable que el diseño de estos tipos evolucione

Evita revelar representaciones concretas de objetos. Por ejemplo, la representación concreta de DateTime valores no se revela mediante la API pública externa del diseño de la biblioteca de .NET. En tiempo de ejecución, Common Language Runtime conoce la implementación confirmada que se usará durante toda la ejecución. Sin embargo, el código compilado no recoge dependencias en la representación concreta.

Evitar el uso de la herencia de implementación para la extensibilidad

En F#, rara vez se usa la herencia de implementación. Además, las jerarquías de herencia suelen ser complejas y difíciles de cambiar cuando llegan nuevos requisitos. La implementación de herencia sigue existiendo en F# para compatibilidad y casos poco frecuentes en los que es la mejor solución a un problema, pero se deben buscar técnicas alternativas en los programas de F# al diseñar para polimorfismo, como la implementación de la interfaz.

Firmas de función y miembro

Usa tuplas para valores devueltos al devolver un pequeño número de varios valores no relacionados

Este es un buen ejemplo de uso de una tupla en un tipo de valor devuelto:

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

Para los tipos devueltos que contienen muchos componentes, o donde los componentes están relacionados con una sola entidad identificable, considere la posibilidad de usar un tipo con nombre en lugar de una tupla.

Uso Async<T> para la programación asincrónica en los límites de la API de F#

Si hay una operación sincrónica correspondiente denominada Operation que devuelve un T, la operación asincrónica debe tener un nombre AsyncOperation si devuelve Async<T> o OperationAsync si devuelve Task<T>. Para los tipos de .NET usados habitualmente que exponen métodos Begin/End, considere la posibilidad de usar Async.FromBeginEnd para escribir métodos de extensión como fachada para proporcionar el modelo de programación asincrónica de F# a esas API de .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() =
        ...

Excepciones

Consulta Administración de errores para obtener información sobre el uso adecuado de excepciones, resultados y opciones.

Miembros de extensión

Aplicar cuidadosamente miembros de extensión de F# en componentes de F#a F#

Por lo general, los miembros de extensión de F# solo se deben usar para las operaciones que están en el cierre de operaciones intrínsecas asociadas a un tipo en la mayoría de sus modos de uso. Un uso común es proporcionar API más idiomáticas a F# para varios 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 uniones

Usar uniones discriminadas en lugar de jerarquías de clases para datos estructurados en árbol

Las estructuras de tipo árbol se definen de forma recursiva. Esto es incómodo con la herencia, pero elegante con uniones discriminadas.

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

La representación de datos similares a árbol con uniones discriminadas también le permite beneficiarse de la exhaustividad en la coincidencia de patrones.

Usa [<RequireQualifiedAccess>] en tipos de unión cuyos nombres de mayúsculas y minúsculas no son suficientemente únicos

Es posible que se encuentre en un dominio donde el mismo nombre es el mejor nombre para diferentes cosas, como casos de unión discriminada. Puedes usar [<RequireQualifiedAccess>] para eliminar la ambigüedad de los nombres de mayúsculas y minúsculas con el fin de evitar que se desencadenen errores confusos debido a la sombra que depende del orden de las instruccionesopen

Oculta las representaciones de uniones discriminadas para las API compatibles con binarios si es probable que el diseño de estos tipos evolucione

Los tipos de uniones se basan en formularios de coincidencia de patrones de F# para un modelo de programación conciso. Como se mencionó anteriormente, debes evitar revelar representaciones de datos concretas si es probable que el diseño de estos tipos evolucione.

Por ejemplo, la representación de una unión discriminada se puede ocultar mediante una declaración privada o interna, o mediante un archivo de firma.

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

Si revelas uniones discriminadas indiscriminadamente, es posible que te resulte difícil versionar la biblioteca sin interrumpir el código de usuario. En su lugar, considera la posibilidad de revelar uno o varios patrones activos para permitir la coincidencia de patrones en los valores del tipo.

Los patrones activos proporcionan una manera alternativa de proporcionar a los consumidores de F# coincidencia de patrones, a la vez que evitan exponer directamente los tipos de unión de F#.

Funciones insertadas y restricciones de miembro

Definir algoritmos numéricos genéricos mediante funciones insertadas con restricciones de miembro implícitas y tipos genéricos resueltos estáticamente

Las restricciones de miembro aritmético y las restricciones de comparación de F# son un estándar para la programación de F#. Por ejemplo, considere el siguiente 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

El tipo de esta función es el siguiente:

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

Se trata de una función adecuada para una API pública en una biblioteca matemática.

Evite el uso de restricciones de miembro para simular clases de tipo y duck typing

Es posible el «duck typing» mediante restricciones de miembro de F#. Sin embargo, los miembros que hacen uso de esto no deben usarse en general en diseños de biblioteca de F#a F#. Esto se debe a que los diseños de biblioteca basados en restricciones implícitas desconocidas o no estándar tienden a hacer que el código de usuario se vuelva inflexible y vinculado a un patrón de marco determinado.

Además, existe una buena posibilidad de que el uso intensivo de restricciones de miembro de esta manera pueda dar lugar a tiempos de compilación muy largos.

Definiciones de operador

Evita definir operadores simbólicos personalizados

Los operadores personalizados son esenciales en algunas situaciones y son dispositivos notación muy útiles dentro de un gran cuerpo de código de implementación. Para los nuevos usuarios de una biblioteca, las funciones con nombre suelen ser más fáciles de usar. Además, los operadores simbólicos personalizados pueden ser difíciles de documentar y los usuarios encuentran más difícil buscar ayuda en los operadores, debido a las limitaciones existentes en el IDE y los motores de búsqueda.

Como resultado, es mejor publicar la funcionalidad como funciones con nombre y miembros, y además exponer operadores para esta funcionalidad solo si las ventajas de la anotación superan la documentación y el coste cognitivo de tenerlas.

Unidades de medida

Usa cuidadosamente unidades de medida para la seguridad de tipos agregada en el código de F#

Se borra información adicional de escritura para unidades de medida cuando se ven en otros lenguajes de .NET. Ten en cuenta que los componentes, las herramientas y la reflexión de .NET verán types-sans-units. Por ejemplo, los consumidores de C# verán float en lugar de float<kg>.

Abreviaturas de tipo

Usa las abreviaturas de tipo cuidadosamente para simplificar el código de F#

Los componentes, las herramientas y la reflexión de .NET no verán nombres abreviados para los tipos. El uso significativo de las abreviaturas de tipo también puede hacer que un dominio parezca más complejo de lo que realmente es, lo que podría confundir a los consumidores.

Evita las abreviaturas de tipo para los tipos públicos cuyos miembros y propiedades deben ser intrínsecamente diferentes a los disponibles en el tipo que se abrevia

En este caso, el tipo que se abrevia revela demasiado sobre la representación del tipo real que se está definiendo. En su lugar, considera la posibilidad de ajustar la abreviatura en un tipo de clase o una unión discriminada de un solo caso (o, cuando el rendimiento es esencial, considera la posibilidad de usar un tipo de estructura para ajustar la abreviatura).

En este caso, es tentador definir un mapa múltiple como un caso especial de un mapa de F#, por ejemplo:

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

Sin embargo, las operaciones lógicas de notación de puntos en este tipo no son las mismas que las operaciones de un mapa; por ejemplo, es razonable que el operador map[key] de búsqueda devuelva la lista vacía si la clave no está en el diccionario, en lugar de generar una excepción.

Directrices para bibliotecas para su uso desde otros lenguajes de .NET

Al diseñar bibliotecas para su uso desde otros lenguajes .NET, es importante cumplir las Directrices de diseño de la biblioteca .NET. En este documento, estas bibliotecas se etiquetan como bibliotecas de .NET de vainilla, en lugar de bibliotecas orientadas a F#, que usan construcciones de F# sin restricciones. Diseñar bibliotecas .NET de vainilla significa proporcionar API familiares e idiomáticas coherentes con el resto de .NET Framework al minimizar el uso de construcciones específicas de F#en la API pública. Estas restricciones se explican en la sección siguiente.

Diseño de espacio de nombres y tipo (para bibliotecas para su uso desde otros lenguajes de .NET)

Aplicación de las convenciones de nomenclatura de .NET a la API pública de los componentes

Presta especial atención al uso de nombres abreviados y las directrices de mayúsculas de .NET.

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

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

Usa espacios de nombres, tipos y miembros como estructura organizativa principal para los componentes

Todos los archivos que contienen la funcionalidad pública deben comenzar con una namespace declaración y las únicas entidades orientadas al público en los espacios de nombres deben ser tipos. No uses módulos de F#.

Usa módulos no públicos para contener código de implementación, tipos de utilidad y funciones de utilidad.

Los tipos estáticos deben ser preferidos sobre los módulos, ya que permiten que la evolución futura de la API use la sobrecarga y otros conceptos de diseño de la API de .NET que no se puedan usar en módulos de F#.

Por ejemplo, en lugar de la siguiente API pública:

module Fabrikam

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

Considera en su lugar:

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 los tipos de registros de F# en las API de .NET en vainilla si el diseño de los tipos no evolucionará

Los tipos de registros de F# se compilan en una clase .NET simple. Son adecuados para algunos tipos simples y estables en las API. Considera la posibilidad de usar los [<NoEquality>] atributos y [<NoComparison>] para suprimir la generación automática de interfaces. Evita también el uso de campos de registro mutables en las API de .NET en vainilla, ya que estos exponen un campo público. Considera siempre si una clase proporcionaría una opción más flexible para la evolución futura de la API.

Por ejemplo, el siguiente código de F# expone la API pública a un 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 la representación de los tipos de unión de F# en las API de .NET de vainilla

Los tipos de unión de F# no se usan normalmente a través de los límites del componente, incluso para la codificación de F#a F#. Son un dispositivo de implementación excelente cuando se usan internamente dentro de componentes y bibliotecas.

Al diseñar una API de .NET en vainilla, considera la posibilidad de ocultar la representación de un tipo de unión mediante una declaración privada o un archivo de firma.

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

También puedes aumentar los tipos que usan una representación de unión internamente con miembros para proporcionar un deseado. API orientada a 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)

Diseño de GUI y otros componentes mediante los patrones de diseño del marco

Hay muchos marcos diferentes disponibles en .NET, como WinForms, WPF y ASP.NET. Las convenciones de nomenclatura y diseño para cada una de ellas deben usarse si está diseñando componentes para su uso en estos marcos. Por ejemplo, para la programación de WPF, adopta patrones de diseño de WPF para las clases que está diseñando. Para los modelos de programación de la interfaz de usuario, usa patrones de diseño como eventos y colecciones basadas en notificaciones, como las que se encuentran en System.Collections.ObjectModel.

Diseño de objetos y miembros (para librerías para usar desde otros lenguajes .NET)

Uso del atributo CLIEvent para exponer eventos de .NET

Construye un DelegateEvent objeto con un tipo de delegado de .NET específico que tome un objeto y EventArgs (en lugar de un Event, que simplemente use el FSharpHandler tipo de forma predeterminada) para que los eventos se publiquen de la manera familiar con otros lenguajes .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

Exponer operaciones asincrónicas como métodos que devuelven tareas de .NET

Las tareas se usan en .NET para representar cálculos asincrónicos activos. En general, las tareas son menos composiciones que los objetos de F# Async<T>, ya que representan tareas «ya en ejecución» y no se pueden componer conjuntamente de maneras que realizan composición paralela o que ocultan la propagación de señales de cancelación y otros parámetros contextuales.

Sin embargo, a pesar de esto, los métodos que devuelven Tasks son la representación estándar de la programación asincrónica en .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

Con frecuencia, también querrá aceptar un token de cancelación 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)

Uso de tipos de delegado de .NET en lugar de tipos de función de F#

Aquí «Tipos de función de F#» significan tipos de «flecha» como int -> int.

En vez de esto:

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

Haga esto:

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

El tipo de función F# aparece como class FSharpFunc<T,U> en otros lenguajes .NET y es menos adecuado para las características de lenguaje y las herramientas que comprenden los tipos delegados. Al crear un método de orden superior destinado a .NET Framework 3.5 o superior, los System.Func delegados y System.Action son las API adecuadas para publicar para permitir que los desarrolladores de .NET consuman estas API de forma de baja fricción. (Cuando el destino es .NET Framework 2.0, los tipos de delegado definidos por el sistema son más limitados; considera la posibilidad de usar tipos delegados predefinidos como System.Converter<T,U> o definir un tipo de delegado específico.)

En el lado contrario, los delegados de .NET no son naturales para las bibliotecas orientadas a F#(consulte la sección siguiente sobre bibliotecas orientadas a F#). Como resultado, una estrategia de implementación común al desarrollar métodos de orden superior para bibliotecas .NET de vainilla es crear toda la implementación mediante tipos de función de F# y, a continuación, crear la API pública mediante delegados como una fachada fina sobre la implementación real de F#.

Usa el patrón TryGetValue en lugar de devolver valores de opción de F# y prefiere la sobrecarga del método para tomar los valores de opción de F# como argumentos

Los patrones comunes de uso para el tipo de opción F# en las API se implementan mejor en las API de .NET en vainilla mediante técnicas de diseño estándar de .NET. En lugar de devolver un valor de opción de F#, considera la posibilidad de usar el tipo de valor devuelto bool más un parámetro out como en el patrón «TryGetValue». Y en lugar de tomar valores de opción de F# como parámetros, considera la posibilidad de usar la sobrecarga de métodos o argumentos opcionales.

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 los tipos de interfaz de colección de .NET IEnumerable<T> e IDictionary<Key,Value> para parámetros y valores devueltos

Evita el uso de tipos de colección concretos, como matrices T[]de .NET, tipos de list<T>Map<Key,Value> F# y , y Set<T>tipos de colección concretos de .NET, como Dictionary<Key,Value>. Las directrices de diseño de la biblioteca .NET tienen buenos consejos sobre cuándo usar varios tipos de colección como IEnumerable<T>. Algunos usos de matrices (T[]) son aceptables en algunas circunstancias, por motivos de rendimiento. Ten en cuenta especialmente que seq<T> es solo el alias de F# para IEnumerable<T>y, por tanto, seq suele ser un tipo adecuado para una API de .NET de vainilla.

En lugar de listas de F#:

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

Usa secuencias de F#:

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

Usa el tipo de unidad como el único tipo de entrada de un método para definir un método de argumento cero, o como el único tipo de valor devuelto para definir un método que devuelva void

Evita otros usos del tipo de unidad. Estos son buenos:

✔ member this.NoArguments() = 3

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

Esto no está bien:

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

Comprobación de valores NULL en límites de la API de .NET en vainilla

El código de implementación de F# tiende a tener menos valores NULL, debido a patrones de diseño inmutables y restricciones en el uso de literales NULL para tipos de F#. Otros lenguajes .NET suelen usar null como valor con mucha más frecuencia. Por este motivo, el código de F# que expone una API de .NET en vainilla debe comprobar los parámetros de null en el límite de la API y evitar que estos valores fluyan más profundos en el código de implementación de F#. Se puede usar la isNull función o la coincidencia de patrones en el null patrón.

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

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

Evita el uso de tuplas como valores devueltos

En su lugar, debes preferir devolver un tipo con nombre que contiene los datos agregados o usar parámetros out para devolver varios valores. Aunque las tuplas y tuplas de estructura existen en .NET (incluida la compatibilidad del lenguaje C# con tuplas de estructura), a menudo no proporcionarán la API ideal y esperada para los desarrolladores de .NET.

Evitar el uso de los parámetros

En su lugar, usa convenciones Method(arg1,arg2,…,argN)de llamada de .NET .

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

Sugerencia: Si vas a diseñar bibliotecas para su uso desde cualquier lenguaje .NET, no hay ningún sustituto de realizar realmente alguna programación experimental de C# y Visual Basic para asegurarte de que las bibliotecas «se sienten bien» desde estos lenguajes. También puedes usar herramientas como Reflector de .NET y el Examinador de objetos de Visual Studio para asegurarte de que las bibliotecas y tu documentación aparezcan según lo previsto para los desarrolladores.

Apéndice

Ejemplo completo de diseño de código de F# para su uso por otros lenguajes .NET

Observa la clase siguiente:

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

El tipo F# inferido de esta clase es el siguiente:

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

Echemos un vistazo a cómo aparece este tipo de F# para un programador mediante otro lenguaje .NET. Por ejemplo, la «firma» de C# aproximada es la siguiente:

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

Hay algunos puntos importantes que se deben tener en cuenta sobre cómo F# representa las construcciones aquí. Por ejemplo:

  • Se han conservado metadatos como nombres de argumento.

  • Los métodos de F# que toman dos argumentos se convierten en métodos de C# que toman dos argumentos.

  • Las funciones y las listas se convierten en referencias a los tipos correspondientes de la biblioteca de F#.

En el código siguiente se muestra cómo ajustar este código para tener en cuenta estas cosas.

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

El tipo F# inferido de esta clase es el siguiente:

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

La firma C# ahora es así:

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

Las correcciones realizadas para preparar este tipo para su uso como parte de una biblioteca de .NET de vainilla son las siguientes:

  • Se ajustaron varios nombres: Point1, n, ly f se convirtieron en RadialPoint, countfactor, y transform, respectivamente.

  • Se usa un tipo de valor devuelto de seq<RadialPoint> en lugar de cambiar una construcción de RadialPoint list lista mediante [ ... ] a una construcción de secuencia mediante IEnumerable<RadialPoint>.

  • Se usa el tipo System.Func de delegado .NET en lugar de un tipo de función de F#.

Esto hace que sea mucho más agradable consumir en código de C#.