c# f#

El mejor enfoque para diseñar bibliotecas F#para usar desde F#y C#



(4)

Estoy tratando de diseñar una biblioteca en F #. La biblioteca debe ser amigable para usar desde F # y C # .

Y aquí es donde estoy estancado un poco. Puedo hacerlo amigable con F #, o puedo hacerlo amigable con C #, pero el problema es cómo hacerlo amigable para ambos.

Aquí hay un ejemplo. Imagina que tengo la siguiente función en F #:

let compose (f: ''T -> ''TResult) (a : ''TResult -> unit) = f >> a

Esto es perfectamente utilizable desde F #:

let useComposeInFsharp() = let composite = compose (fun item -> item.ToString) (fun item -> printfn "%A" item) composite "foo" composite "bar"

En C #, la función de compose tiene la siguiente firma:

FSharpFunc<T, Unit> compose<T, TResult>(FSharpFunc<T, TResult> f, FSharpFunc<TResult, Unit> a);

Pero, por supuesto, no quiero FSharpFunc en la firma, lo que quiero es Func y Action lugar, así:

Action<T> compose2<T, TResult>(Func<T, TResult> f, Action<TResult> a);

Para lograr esto, puedo crear la función compose2 esta manera:

let compose2 (f: Func<''T, ''TResult>) (a : Action<''TResult> ) = new Action<''T>(f.Invoke >> a.Invoke)

Ahora esto es perfectamente utilizable en C #:

void UseCompose2FromCs() { compose2((string s) => s.ToUpper(), Console.WriteLine); }

¡Pero ahora tenemos problemas al usar compose2 desde F #! Ahora tengo que ajustar todos los funcionamientos estándar de F # en Func y Action , así:

let useCompose2InFsharp() = let f = Func<_,_>(fun item -> item.ToString()) let a = Action<_>(fun item -> printfn "%A" item) let composite2 = compose2 f a composite2.Invoke "foo" composite2.Invoke "bar"

La pregunta: ¿cómo podemos lograr una experiencia de primera clase para la biblioteca escrita en F # para los usuarios F # y C #?

Hasta ahora, no he podido encontrar nada mejor que estos dos enfoques:

  1. Dos ensamblajes separados: uno dirigido a usuarios de F # y el segundo a usuarios de C #.
  2. Un ensamblado pero diferentes espacios de nombres: uno para usuarios de F # y el segundo para usuarios de C #.

Para el primer acercamiento, haría algo como esto:

  1. Crea un proyecto F #, llámalo FooBarFs y compilalo en FooBarFs.dll.

    • Diríjase a la biblioteca exclusivamente a usuarios de F #.
    • Ocultar todo lo innecesario de los archivos .fsi.
  2. Cree otro proyecto F #, llame si FooBarCs y compile en FooFar.dll

    • Reutilice el primer proyecto F # en el nivel de origen.
    • Crea un archivo .fsi que oculte todo de ese proyecto.
    • Cree el archivo .fsi que expone la biblioteca en formato C #, usando expresiones idiomáticas C # para nombre, espacios de nombres, etc.
    • Cree envolturas que deleguen en la biblioteca central, realizando la conversión cuando sea necesario.

Creo que el segundo enfoque con los espacios de nombres puede ser confuso para los usuarios, pero luego tiene un ensamblaje.

La pregunta: ninguno de estos es ideal, tal vez me falta algún tipo de indicador de compilación / switch / attribte o algún tipo de truco y hay una mejor manera de hacerlo.

La pregunta: ¿ alguien más ha tratado de lograr algo similar y si es así cómo lo hiciste?

EDITAR: para aclarar, la pregunta no es solo sobre funciones y delegados, sino sobre la experiencia general de un usuario de C # con una biblioteca F #. Esto incluye espacios de nombres, convenciones de nombres, expresiones idiomáticas y cosas por el estilo que son nativas de C #. Básicamente, un usuario de C # no debería poder detectar que la biblioteca fue creada en F #. Y viceversa, un usuario F # debería sentirse como tratar con una biblioteca C #.

EDICION 2:

Puedo ver a partir de las respuestas y los comentarios hasta ahora que mi pregunta carece de la profundidad necesaria, tal vez principalmente debido al uso de un solo ejemplo donde surgen problemas de interoperabilidad entre F # y C #, la cuestión de las funciones es un valor. Creo que este es el ejemplo más obvio, así que esto me llevó a usarlo para hacer la pregunta, pero por la misma razón daba la impresión de que este es el único problema que me preocupa.

Permítanme proporcionar ejemplos más concretos. He leído el documento más excelente de F # Component Design Guidelines (muchas gracias @gradbot por esto!). Las pautas en el documento, si se usan, abordan algunos de los problemas, pero no todos.

El documento se divide en dos partes principales: 1) pautas para identificar a los usuarios de F #; y 2) pautas para apuntar a usuarios de C #. En ninguna parte intenta siquiera pretender que sea posible tener un enfoque uniforme, lo que hace eco exactamente a mi pregunta: podemos apuntar a F #, podemos apuntar a C #, pero ¿cuál es la solución práctica para apuntar a ambos?

Para recordar, el objetivo es tener una biblioteca creada en F #, y que se pueda usar de forma idiomática tanto en los idiomas F # como C #.

La palabra clave aquí es idiomática . El problema no es la interoperabilidad general donde solo es posible usar bibliotecas en diferentes idiomas.

Ahora los ejemplos, que tomo directamente de F # Component Design Guidelines .

  1. Módulos + funciones (F #) vs Espacios de nombre + Tipos + funciones

    • F #: Utiliza espacios de nombres o módulos para contener tus tipos y módulos. El uso idiomático es colocar funciones en módulos, por ejemplo:

      // library module Foo let bar() = ... let zoo() = ... // Use from F# open Foo bar() zoo()

    • C #: utilice espacios de nombres, tipos y miembros como la estructura organizativa principal para sus componentes (a diferencia de los módulos), para las API .NET de vanilla.

      Esto es incompatible con la guía F #, y el ejemplo debería volver a escribirse para adaptarse a los usuarios de C #:

      [<AbstractClass; Sealed>] type Foo = static member bar() = ... static member zoo() = ...

      Sin embargo, al hacerlo, rompemos el uso idiomático de F # porque ya no podemos usar la bar y el zoo sin el prefijo Foo .

  2. Uso de tuplas

    • F #: utilice tuplas cuando sea apropiado para los valores de retorno.

    • C #: Evite usar tuplas como valores de retorno en las API .NET de vanilla.

  3. Async

    • F #: Utiliza Async para la programación asíncrona en los límites de la API F #.

    • C #: Exponga las operaciones asincrónicas utilizando el modelo de programación asíncrona .NET (BeginFoo, EndFoo), o como métodos que devuelven tareas .NET (Tarea), en lugar de objetos F asincrónicos.

  4. Uso de la Option

    • F #: Considere el uso de valores de opción para los tipos de devolución en lugar de aumentar las excepciones (para el código de facción F #).

    • Considere utilizar el patrón TryGetValue en lugar de devolver los valores de la opción F # (opción) en las API .NET de vanilla, y prefiera la sobrecarga de métodos para tomar valores de opción F # como argumentos.

  5. Uniones discriminadas

    • F #: Utilice uniones discriminadas como alternativa a las jerarquías de clases para crear datos estructurados por árbol

    • C #: no hay pautas específicas para esto, pero el concepto de uniones discriminadas es ajeno a C #

  6. Funciones al curry

    • F #: las funciones al curry son idiomáticas para F #

    • C #: No use currying de parámetros en las API .NET de vanilla.

  7. Comprobando valores nulos

    • F #: esto no es idiomático para F #

    • C #: Considere la posibilidad de verificar valores nulos en los límites de API .NET de vanilla.

  8. Uso de la list tipos F #, map , set , etc.

    • F #: es idiomático usar estos en F #

    • C #: Considere usar los tipos de interfaz de colección .NET IEnumerable e IDictionary para los parámetros y los valores de retorno en las API .NET de vanilla. ( es decir, no use la list F #, map , set )

  9. Tipos de funciones (el más obvio)

    • F #: uso de funciones F # ya que los valores son idiomáticos para F #, obvio

    • C #: utilice los tipos de delegados .NET en lugar de los tipos de funciones F # en las API .NET de vanilla.

Creo que esto debería ser suficiente para demostrar la naturaleza de mi pregunta.

Por cierto, las pautas también tienen una respuesta parcial:

... una estrategia de implementación común al desarrollar métodos de orden superior para las bibliotecas vanilla .NET es crear toda la implementación utilizando tipos de funciones F #, y luego crear la API pública utilizando delegados como una fachada delgada encima de la implementación real de F #.

Resumir.

Hay una respuesta definitiva: no faltan trucos de compilación .

Según el documento de directrices, parece que la creación de un contenedor de fachada para .NET es la estrategia razonable.

La pregunta entonces permanece con respecto a la implementación práctica de esto:

  • Asambleas separadas? o

  • ¿Diferentes espacios de nombres?

Si mi interpretación es correcta, Tomas sugiere que el uso de espacios de nombres separados debería ser suficiente y debería ser una solución aceptable.

Creo que estaré de acuerdo con eso dado que la elección de los espacios de nombres es tal que no sorprende ni confunde a los usuarios de .NET / C #, lo que significa que el espacio de nombres para ellos probablemente debería parecer que es el espacio de nombres primario para ellos. Los usuarios de F # tendrán que asumir la carga de elegir el espacio de nombres específico de F #. Por ejemplo:

  • FSharp.Foo.Bar -> espacio de nombres para la biblioteca de F # frente

  • Foo.Bar -> namespace para .NET wrapper, idiomatic para C #


Aunque probablemente sería una exageración, podría considerar escribir una aplicación usando Mono.Cecil (tiene una compatibilidad increíble en la lista de correo) que automatizaría la conversión en el nivel IL. Por ejemplo, implementa su ensamblaje en F #, usando la API pública de estilo F #, luego la herramienta generaría un envoltorio amigo de C # sobre él.

Por ejemplo, en F # obviamente usaría la option<''T> ( None , específicamente) en lugar de usar null como en C #. Escribir un generador de envoltura para este escenario debería ser bastante fácil: el método de envoltura invocaría el método original: si su valor de retorno era Some x , entonces devuelva x , de lo contrario, devuelva null .

Debería manejar el caso cuando T es un tipo de valor, es decir, no admite nulos; Tendría que ajustar el valor de retorno del método de envoltura en Nullable<T> , lo que lo hace un poco doloroso.

Nuevamente, estoy bastante seguro de que sería rentable escribir una herramienta de este tipo en su escenario, tal vez, excepto si trabajará en esta biblioteca (utilizable a la perfección desde F # y C # ambas) regularmente. En cualquier caso, creo que sería un experimento interesante, uno que incluso podría explorar en algún momento.


Daniel ya explicó cómo definir una versión amigable para C # de la función F # que usted escribió, así que agregaré algunos comentarios de mayor nivel. En primer lugar, debe leer las research.microsoft.com/en-us/um/cambridge/projects/fsharp/… (referenciadas ya por gradbot). Este es un documento que explica cómo diseñar bibliotecas F # y .NET usando F # y debería responder muchas de sus preguntas.

Al usar F #, hay básicamente dos tipos de bibliotecas que puede escribir:

  • La biblioteca F # está diseñada para ser utilizada solo desde F #, por lo que su interfaz pública está escrita en un estilo funcional (utilizando tipos de funciones F #, tuplas, uniones discriminadas, etc.)

  • La biblioteca .NET está diseñada para ser utilizada desde cualquier lenguaje .NET (incluyendo C # y F #) y típicamente sigue el estilo orientado a objetos .NET. Esto significa que expondrá la mayor parte de la funcionalidad como clases con método (y, a veces, métodos de extensión o métodos estáticos, pero principalmente el código debe escribirse en el diseño OO).

En su pregunta, usted está preguntando cómo exponer la composición de funciones como una biblioteca .NET, pero creo que funciones como su compose son conceptos de nivel demasiado bajo desde el punto de vista de la biblioteca .NET. Puede exponerlos como métodos que funcionan con Func y Action , pero probablemente no es así como diseñaría una biblioteca normal .NET en primer lugar (tal vez usaría el patrón Builder en su lugar o algo así).

En algunos casos (es decir, cuando se diseñan bibliotecas numéricas que no encajan bien con el estilo de la biblioteca .NET), tiene sentido diseñar una biblioteca que mezcle estilos F # y .NET en una sola biblioteca. La mejor manera de hacerlo es tener API normal F # (o .NET normal) y luego proporcionar envoltorios para uso natural en el otro estilo. Los contenedores pueden estar en un espacio de nombre separado (como MyLibrary.FSharp y MyLibrary ).

En su ejemplo, puede dejar la implementación F # en MyLibrary.FSharp y luego agregar envoltorios .NET (C # -friendly) (similar al código que publicó Daniel) en el espacio de nombres MyLibrary como método estático de alguna clase. Pero, de nuevo, la biblioteca .NET probablemente tenga una API más específica que la composición de la función.


Solo tiene que ajustar los valores de función (funciones parcialmente aplicadas, etc.) con Func o Action , el resto se convierte automáticamente. Por ejemplo:

type A(arg) = member x.Invoke(f: Func<_,_>) = f.Invoke(arg) let a = A(1) a.Invoke(fun i -> i + 1)

Entonces tiene sentido usar Func / Action donde corresponda. ¿Esto elimina tus preocupaciones? Creo que las soluciones propuestas son demasiado complicadas. Puedes escribir toda tu biblioteca en F # y usarla sin dolor desde F # y C # (lo hago todo el tiempo).

Además, F # es más flexible que C # en términos de interoperabilidad, por lo que generalmente es mejor seguir el estilo .NET tradicional cuando esto es una preocupación.

EDITAR

El trabajo requerido para crear dos interfaces públicas en espacios de nombres separados, creo, solo se justifica cuando son complementarios o la funcionalidad F # no se puede utilizar desde C # (como las funciones inline, que dependen de los metadatos específicos de F #).

Tomando sus puntos a su vez:

  1. Módulo + let enlaces y tipos sin constructor + miembros estáticos aparecen exactamente iguales en C #, así que ve con módulos si puedes. Puede usar CompiledNameAttribute para darles a los miembros C # -friendly names.

  2. Puedo estar equivocado, pero creo que las Pautas de los componentes se escribieron antes de que System.Tuple se agregara al marco. (En versiones anteriores, F # definió su propio tipo de tupla). Desde entonces, se ha vuelto más aceptable usar Tuple en una interfaz pública para tipos triviales.

  3. Aquí es donde creo que tienes que hacer las cosas al estilo C # porque F # funciona bien con la Task pero C # no funciona bien con Async . Puede utilizar async internamente y luego llamar a Async.StartAsTask antes de regresar de un método público.

  4. El abrazo de null puede ser el mayor inconveniente al desarrollar una API para usar desde C #. En el pasado, probé todo tipo de trucos para evitar considerar nulo en el código F # interno, pero, al final, era mejor marcar los tipos con constructores públicos con [<AllowNullLiteral>] y verificar args para null. No es peor que C # a este respecto.

  5. Los sindicatos discriminados generalmente se compilan en jerarquías de clase, pero siempre tienen una representación relativamente amistosa en C #. Yo diría, [<AllowNullLiteral>] con [<AllowNullLiteral>] y [<AllowNullLiteral>] .

  6. Las funciones curried producen valores de función, que no deberían usarse.

  7. Descubrí que era mejor adoptar un valor nulo que depender de que lo atraparan en la interfaz pública e ignorarlo internamente. YMMV.

  8. Tiene mucho sentido usar list / map / set internamente. Todos pueden exponerse a través de la interfaz pública como IEnumerable<_> . Además, seq , dict y Seq.readonly son frecuentemente útiles.

  9. Ver # 6.

La estrategia que elija depende del tipo y tamaño de su biblioteca pero, en mi experiencia, encontrar el punto ideal entre F # y C # requirió menos trabajo a largo plazo que crear API separadas.


research.microsoft.com/en-us/um/cambridge/projects/fsharp/…

Descripción general Este documento analiza algunos de los problemas relacionados con el diseño y la codificación del componente F #. En particular, cubre:

  • Pautas para diseñar bibliotecas .NET "vanilla" para usar desde cualquier lenguaje .NET.
  • Pautas para las bibliotecas F # -to-F # y el código de implementación F #.
  • Sugerencias sobre las convenciones de codificación para el código de implementación de F #