2012-04-11 18 views
92

Estoy intentando diseñar una biblioteca en F #. La biblioteca debe ser amigable para usar desde tanto F # como C#.El mejor enfoque para diseñar bibliotecas F # para utilizar 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. Imagino que tengo la siguiente función en C#:

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

Esto es perfectamente utilizable a partir de F #:

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

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

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

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

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

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

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

Ahora bien, esto es perfectamente utilizable en C#:

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

Pero ahora hemos problema usando compose2 de F # ! Ahora tengo que envolver toda norma F # funs 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 C# para los dos F # y C# usuarios?

Hasta el momento, no podía llegar a nada mejor que estos dos enfoques:

  1. dos conjuntos separados: uno dirigido a los usuarios de F #, y la segunda a los usuarios de C#.
  2. Un conjunto pero diferentes espacios de nombres: uno para usuarios F # y el segundo para usuarios C#.

Para el primer enfoque, me gustaría hacer algo como esto:

  1. Crear proyecto F #, lo llaman FooBarFs y compilarlo en FooBarFs.dll.

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

    • Reutiliza el primer proyecto F # en el nivel de la fuente.
    • Crea el archivo .fsi que oculta todo lo de ese proyecto.
    • Crear un archivo .fsi que expone la biblioteca en C#, usando expresiones C# para nombres, 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 entonces usted tiene un ensamblaje.

La pregunta: ninguno de estos son ideales, tal vez me falta algún tipo de indicador del compilador/interruptor/attribte o algún tipo de truco y hay una mejor manera de hacer esto?

La pregunta: ¿Alguien más ha intentado lograr algo similar y, en caso afirmativo, cómo lo hizo?

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


EDIT 2:

puedo ver en las respuestas y comentarios tan lejos que mi pregunta carece de la profundidad necesaria, quizás debido sobre todo al uso de un único ejemplo en el que los problemas de interoperabilidad entre F # y C# surgen, la cuestión de las funciones es un valor. Creo que este es el ejemplo más obvio, así que este me llevó a usarlo para hacer la pregunta, pero por la misma razón daba la impresión de que esto es el único problema que me preocupa.

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

El documento está dividido en dos partes principales: 1) pautas para la orientación de usuarios F #; y 2) pautas para dirigidas a usuarios de C#. En ninguna parte incluso intenta pretender que es 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 de autor en F #, y que puede ser utilizado idiomáticamente de tanto F # y C# idiomas.

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

Ahora a los ejemplos, que tomo directamente de .

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

    • F #: Foro de espacios de nombres o módulos de uso para contener sus 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#: Realice espacios de nombres de uso, tipos y miembros como la estructura organizativa principal para sus componentes (a diferencia de los módulos), de vainilla .NET APIs

      Esto es incompatible con el # directriz F, y el ejemplo necesitaría que ser re-escrita para adaptarse a los C# usuarios:

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

      Al hacerlo, sin embargo, que rompen el uso idiomático de F #, porque ya no podemos usar bar y zoo sin ponerle el prefijo Foo.

  2. uso de tuplas

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

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

  3. asíncrono

    • F #: Hacer uso asíncrono para la programación asincrónica en F # límites de la API.

    • C#: exponga operaciones asíncronas utilizando ya sea el modelo de programación asíncrona .NET (BeginFoo, EndFoo), o como métodos que regresan tareas .NET (tarea), más que como F # asíncrono objetos.

  4. El uso de Option

    • F #: Considere el uso de valores de opciones para los tipos de retorno en lugar de elevar excepciones (para F # Código -frente).

    • Considere utilizar el patrón TryGetValue en lugar de devolver valores de opción F # (opción) en las API .NET de vainilla, y prefiera sobrecargar el método para tomar valores de opción F # como argumentos.

  5. sindicatos discriminado

    • F #: Hacer uso discriminado a los sindicatos como una alternativa a las jerarquías de clases para crear datos de estructura de árbol

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

  6. funciones al curry

    • F #: funciones al curry son idiomática para F #

    • C#: No utilice currificación de parámetros en las API de vainilla .NET.

  7. Comprobación de valores nulos

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

    • C#: Considere la comprobación de valores nulos en los límites de la API de vainilla .NET.

  8. El uso de F # tipos de list, map, set, etc

    • F #: es idiomática utilizar estos en F #

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

  9. tipos de función (la más obvia)

    • F #: uso de F funciones # como valores es idiomático para F #, Obviamente

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

Creo que estos deberían ser suficientes para demostrar la naturaleza de mi pregunta.

Por cierto, las directrices también tienen respuesta parcial:

... una estrategia de aplicación común en el desarrollo de orden superior métodos para bibliotecas de vainilla .NET es al autor toda la aplicación utilizando los tipos de función F #, y luego crea la API pública usando delegados como una fachada delgada encima de la implementación real de F #.

Resumir.

Hay una respuesta definitiva: no hay trucos de compilación que olvidé.

Según el documento de directrices, parece que crear una envoltura de fachada para .NET es la estrategia razonable al crear F # primero y luego crear .

La pregunta entonces permanece sobre la aplicación práctica de esta:

  • 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 debe ser una solución aceptable.

creo que estarán de acuerdo con que, dado que la elección de espacios de nombres es tal que no sorprende o confundir el .NET/C# usuarios, lo que significa que el espacio de nombres para ellos, probablemente, debe parecer que es el principal espacio de nombres 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 F # frente biblioteca

  • Foo.Bar -> espacio de nombres para el envoltorio NET, idiomática para C#

+8

Algo relevante http://research.microsoft.com/en-us/um/cambridge/projects/fsharp/manual/fsharp-component-design-guidelines.pdf – gradbot

+0

Aparte de pasar por las funciones de primera clase, ¿qué ¿tus preocupaciones? F # ya va un largo camino para proporcionar una experiencia fluida de otros idiomas. – Daniel

+0

Re: su edición, todavía no estoy seguro de por qué cree que una biblioteca creada en F # parecería no estándar desde C#. En su mayor parte, F # proporciona un superconjunto de la funcionalidad de C#. Hacer que una biblioteca se sienta natural es principalmente una cuestión de convención, lo cual es cierto sin importar el idioma que esté usando. Todo eso para decir, F # proporciona todas las herramientas que necesita para hacer esto. – Daniel

Respuesta

15

Usted solo tiene que ajustar la función valores (funciones parcialmente aplicadas, etc.) con Func o Action, el resto se convierten 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) 

Por lo tanto, tiene sentido utilizar Func/Action en su caso. ¿Esto elimina tus preocupaciones? Creo que las soluciones propuestas son demasiado complicadas. Puede escribir toda su biblioteca en F # y usarla sin dolor de 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 necesario para hacer dos interfaces públicas en espacios de nombres separados, pienso, solamente se justifica cuando son complementarias o el # funcionalidad F no es utilizable a partir de C# (tales como funciones inline, que dependen de F # metadatos específicos).

Tomando sus puntos a su vez:

  1. Módulo + let fijaciones y de tipo constructor-less + miembros estáticos parece exactamente lo mismo en C#, así que ve con módulos si se puede. Puede usar CompiledNameAttribute para darles a los miembros C# -friendly names.

  2. Puedo estar equivocado, pero creo que las Pautas de componentes se escribieron antes de que se agregara System.Tuple 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 Task pero C# no funciona bien con Async. Puede utilizar async internamente y luego llamar al 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 interno de F # pero, al final, lo mejor era marcar los tipos con constructores públicos con [<AllowNullLiteral>] y verificar args para null. No es peor que C# a este respecto.

  5. Las uniones discriminadas generalmente se compilan a las jerarquías de clase, pero siempre tienen una representación relativamente amigable en C#. Yo diría, márcalos con [<AllowNullLiteral>] y úsalos.

  6. Las funciones almenadas producen la función valores, que no deben utilizarse.

  7. Encontré que era mejor abrazar el nulo que depender de que sea capturado en la interfaz pública e ignorarlo internamente. YMMV.

  8. Tiene mucho sentido utilizar 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.

Qué estrategia que se utiliza depende del tipo y tamaño de la biblioteca, pero, en mi experiencia, encontrar el punto óptimo entre F # y C# requiere menos trabajo en el largo plazo, que la creación de APIs separadas.

+0

sí, puedo ver que hay algunas maneras de hacer que las cosas funcionen, no estoy del todo convencido. Re módulos. Si tienes el 'módulo Foo.Bar.Zoo' luego en C# esto será' clase Foo' en el espacio de nombres 'Foo.Bar'. Sin embargo, si tiene módulos anidados como 'módulo Foo = ... módulo Bar = ... módulo Zoo ==', esto generará la clase 'Foo' con las clases internas' Bar' y 'Zoo'. Re 'IEnumerable', esto puede no ser aceptable cuando realmente necesitamos trabajar con mapas y conjuntos. Re 'null' values ​​y' AllowNullLiteral' - una de las razones por las que me gusta F # es porque estoy cansado de 'null' en C#, ¡realmente no querría contaminar todo con él otra vez! –

+0

Generalmente, se favorecen espacios de nombres sobre módulos anidados para interoperabilidad. Re: null, estoy de acuerdo con usted, sin duda es una regresión para tener que considerarlo, pero hay algo con lo que tiene que lidiar si su API se utilizará desde C#. – Daniel

33

Daniel ya explicó cómo definir una versión amigable para C# de la función F # que escribió, por lo que agregaré algunos comentarios de mayor nivel. Antes que nada, debes leer el F# Component Design Guidelines (referenciado 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 se puede escribir:

  • F # biblioteca está diseñado para ser utilizado única de F #, así que ha interfaz pública está escrito en un funcional estilo (usando F # tipos de función, tuplas, discriminados sindicatos etc.)

  • .NET biblioteca está diseñado para ser utilizado 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 funciona como su compose son conceptos demasiado bajo nivel desde el punto de vista de la biblioteca .NET. Puede exponerlos como métodos que trabajan con Func y Action, pero esa no es la forma en que diseñaría una biblioteca normal .NET en primer lugar (quizás usaría el patrón Builder en su lugar o algo así).

En algunos casos (por ejemplo, en el diseño de bibliotecas numéricas que en realidad no encajan bien con el estilo NET), que hace un buen sentido para diseñar una biblioteca que mezcla tanto F # y .NET estilos en una única 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 nombres separado (como MyLibrary.FSharp y MyLibrary).

En su ejemplo, puede dejar la implementación F # en MyLibrary.FSharp y luego agregar .NET (C# -friendly) envoltorios (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.

2

Draft F# Component Design Guidelines (August 2010)

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

  • Directrices para el diseño de bibliotecas .NET "vanilla" para usar desde cualquier lenguaje .NET.
  • Directrices para bibliotecas F # -to ​​F # y código de implementación F #.
  • sugerencias sobre las convenciones de codificación para F # código aplicación
2

Aunque probablemente sería una exageración, usted podría considerar escribir una aplicación que utiliza Mono.Cecil (que tiene soporte impresionante en la lista de correo) que automatizar 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 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.

De nuevo, estoy bastante seguro de que valdría la pena escribir dicha herramienta en su escenario, tal vez excepto si va a 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.

+0

suena interesante. ¿Usaría 'Mono.Cecil' significa básicamente escribir un programa para cargar el ensamble F #, encontrar elementos que requieran mapeo y que ellos emitan otro ensamblaje con los envoltorios C#? ¿Lo entendí bien? –

+0

@Komrade P .: También puedes hacerlo, pero eso es mucho más complicado, porque entonces tendrías que ajustar todas las referencias para señalar a los miembros del nuevo ensamblaje. Es mucho más simple si solo agrega los nuevos miembros (los envoltorios) al ensamblaje F # escrito existente. – ShdNx