Estoy tratando de diseñar una biblioteca en F #. La biblioteca debe ser amigable para su uso tanto en F # como en C # .
Y aquí es donde estoy un poco atrapado. Puedo hacerlo compatible con F #, o puedo hacerlo compatible con C #, pero el problema es cómo hacerlo compatible con 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 compose
función tiene la siguiente firma:
FSharpFunc<T, Unit> compose<T, TResult>(FSharpFunc<T, TResult> f, FSharpFunc<TResult, Unit> a);
Pero claro, no quiero FSharpFunc
en la firma, lo que quiero es Func
y en Action
cambio, así:
Action<T> compose2<T, TResult>(Func<T, TResult> f, Action<TResult> a);
Para lograr esto, puedo crear una compose2
función como esta:
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 un problema al usar compose2
desde F #! Ahora tengo que envolver todos los F # estándar 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 F # para los usuarios de F # y C #?
Hasta ahora, no pude encontrar nada mejor que estos dos enfoques:
- Dos ensamblados separados: uno dirigido a usuarios de F # y el segundo a usuarios de C #.
- Un ensamblado pero diferentes espacios de nombres: uno para usuarios de F # y el segundo para usuarios de C #.
Para el primer enfoque, haría algo como esto:
Cree un proyecto de F #, llámelo FooBarFs y compílelo en FooBarFs.dll.
- Apunte la biblioteca exclusivamente a usuarios de F #.
- Oculte todo lo innecesario de los archivos .fsi.
Cree otro proyecto de F #, llame si FooBarCs y compílelo en FooFar.dll
- Reutilice el primer proyecto de F # en el nivel de origen.
- Cree un archivo .fsi que oculta todo de ese proyecto.
- Cree un archivo .fsi que exponga la biblioteca en forma C #, utilizando modismos C # para el nombre, espacios de nombres, etc.
- Cree contenedores que deleguen a 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 ensamblado.
La pregunta: Ninguno de estos es ideal, tal vez me falta algún tipo de indicador / conmutador / atributo del compilador o algún tipo de truco y ¿hay una mejor manera de hacerlo?
La pregunta: ¿ alguien más ha intentado lograr algo similar y, de ser así, cómo lo hizo?
EDITAR: para aclarar, la pregunta no es solo sobre funciones y delegados, sino también sobre la experiencia general de un usuario de C # con una biblioteca de F #. Esto incluye espacios de nombres, convenciones de nombres, modismos y similares que son nativos de C #. Básicamente, un usuario de C # no debería poder detectar que la biblioteca se creó en F #. Y viceversa, un usuario de F # debería tener ganas de lidiar con una biblioteca de C #.
EDITAR 2:
Puedo ver en las respuestas y comentarios hasta ahora que mi pregunta carece de la profundidad necesaria, quizás principalmente debido al uso de un solo ejemplo donde surgen problemas de interoperabilidad entre F # y C #, el problema de los valores de función. Creo que este es el ejemplo más obvio y, por lo tanto, esto me llevó a usarlo para hacer la pregunta, pero de la misma manera me dio la impresión de que este es el único tema que me preocupa.
Permítanme brindar ejemplos más concretos. He leído el documento más excelente de Pautas de diseño de componentes de F # (¡muchas gracias @gradbot por esto!). Las pautas del documento, si se utilizan, abordan algunos de los problemas, pero no todos.
El documento se divide en dos partes principales: 1) pautas para dirigirse a los usuarios de F #; y 2) pautas para dirigirse a usuarios de C #. En ninguna parte intenta siquiera fingir que es posible tener un enfoque uniforme, lo que se hace eco exactamente de 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 idiomáticamente en los lenguajes F # y C #.
La palabra clave aquí es idiomática . El problema no es la interoperabilidad general donde es posible utilizar bibliotecas en diferentes idiomas.
Ahora a los ejemplos, que tomo directamente de F # Component Design Guidelines .
Módulos + funciones (F #) vs espacios de nombres + tipos + funciones
F #: use espacios de nombres o módulos 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 #: use espacios de nombres, tipos y miembros como la estructura organizativa principal para sus componentes (a diferencia de los módulos), para las API vanilla .NET.
Esto es incompatible con la guía de F #, y el ejemplo debería reescribirse 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
bar
yzoo
sin prefijarlo conFoo
.
Uso de tuplas
F #: use tuplas cuando sea apropiado para los valores de retorno.
C #: Evite el uso de tuplas como valores de retorno en las API de .NET vanilla.
Async
F #: utilice Async para la programación asincrónica en los límites de la API de F #.
C #: exponga las operaciones asincrónicas utilizando el modelo de programación asincrónica de .NET (BeginFoo, EndFoo) o como métodos que devuelven tareas de .NET (Task), en lugar de como objetos F # Async.
Uso de
Option
F #: considere usar valores de opción para los tipos de retorno en lugar de generar excepciones (para el código con orientación F #).
Considere usar el patrón TryGetValue en lugar de devolver valores de opción de F # (opción) en las API vanilla .NET, y prefiera la sobrecarga de métodos en lugar de tomar valores de opción de F # como argumentos.
Sindicatos discriminados
F #: use uniones discriminadas como una alternativa a las jerarquías de clases para crear datos estructurados en árbol
C #: no hay pautas específicas para esto, pero el concepto de uniones discriminadas es ajeno a C #
Funciones al curry
F #: las funciones al curry son idiomáticas para F #
C #: no utilice la conversión de parámetros en las API de .NET vanilla.
Comprobando valores nulos
F #: esto no es idiomático para F #
C #: considere la posibilidad de verificar valores nulos en los límites de la API de .NET estándar.
El uso de tipos F #
list
,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 parámetros y valores de retorno en las API vanilla .NET. ( Es decir, no utilizan F #
list
,map
,set
)
Tipos de funciones (la obvia)
F #: el uso de funciones de F # como valores es idiomático para F #, obviamente
C #: utilice los tipos de delegado .NET en lugar de los tipos de función F # en las API de .NET 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 bibliotecas .NET vanilla es crear toda la implementación usando tipos de función F #, y luego crear la API pública usando delegados como una fachada delgada sobre la implementación real de F #.
Resumir.
Hay una respuesta definitiva: no hay trucos del compilador que me haya perdido .
De acuerdo con el documento de directrices, parece que la creación de F # primero y luego la creación de un contenedor de fachada para .NET es una 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 usar 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 verse como el espacio de nombres principal para ellos. Los usuarios de F # tendrán que asumir la carga de elegir un espacio de nombres específico de F #. Por ejemplo:
FSharp.Foo.Bar -> espacio de nombres para F # frente a la biblioteca
Foo.Bar -> espacio de nombres para el contenedor .NET, idiomático para C #
Respuestas:
Daniel ya explicó cómo definir una versión compatible con C # de la función F # que escribiste, así que agregaré algunos comentarios de nivel superior. En primer lugar, debe leer las Pautas de diseño de componentes de F # (a las que ya hace referencia gradbot). Este es un documento que explica cómo diseñar bibliotecas de F # y .NET usando F # y debería responder muchas de sus preguntas.
Cuando usa F #, hay básicamente dos tipos de bibliotecas que puede escribir:
La biblioteca F # está diseñada para usarse solo desde F #, por lo que su interfaz pública está escrita en un estilo funcional (usando tipos de función F #, tuplas, uniones discriminadas, etc.)
La biblioteca .NET está diseñada para usarse desde cualquier lenguaje .NET (incluidos C # y F #) y normalmente 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, está preguntando cómo exponer la composición de funciones como una biblioteca .NET, pero creo que funciones como la suya
compose
son conceptos de nivel demasiado bajo desde el punto de vista de la biblioteca .NET. Puede exponerlos como métodos que funcionan conFunc
yAction
, pero probablemente no sea así como diseñaría una biblioteca .NET normal 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 realmente bien con el estilo de la biblioteca .NET), tiene sentido diseñar una biblioteca que combine los estilos F # y .NET en una sola biblioteca. La mejor manera de hacer esto es tener una API normal de 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
yMyLibrary
).En su ejemplo, podría dejar la implementación de F #
MyLibrary.FSharp
y luego agregar envoltorios .NET (compatibles con C #) (similares al código que publicó Daniel) en elMyLibrary
espacio de nombres como método estático de alguna clase. Pero nuevamente, la biblioteca .NET probablemente tendría una API más específica que la composición de funciones.fuente
Solo tiene que envolver los valores de las funciones ( funciones parcialmente aplicadas, etc.) con
Func
oAction
, el resto se convierte automáticamente. Por ejemplo:Por lo tanto, tiene sentido usar
Func
/Action
donde corresponda. ¿Elimina esto sus preocupaciones? Creo que las soluciones que proponen son demasiado complicadas. Puede escribir toda su 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 tradicional de .NET cuando esto es una preocupación.
EDITAR
El trabajo requerido para hacer dos interfaces públicas en espacios de nombres separados, creo, solo se justifica cuando son complementarias o la funcionalidad de F # no se puede usar desde C # (como las funciones en línea, que dependen de metadatos específicos de F #).
Tomando sus puntos a su vez:
Los
let
enlaces de módulo + y los miembros estáticos de tipo sin constructor + aparecen exactamente iguales en C #, así que vaya con los módulos si puede. Puede utilizarCompiledNameAttribute
para dar a los miembros nombres compatibles con C #.Puede que me equivoque, pero supongo que las Directrices de los componentes se redactaron antes de
System.Tuple
agregarse al marco. (En versiones anteriores, F # definía su propio tipo de tupla). Desde entonces, se ha vuelto más aceptable para usarTuple
en una interfaz pública para tipos triviales.Aquí es donde creo que debes hacer las cosas de la manera C # porque F # juega bien
Task
pero C # no funciona bienAsync
. Puede usar async internamente y luego llamarAsync.StartAsTask
antes de regresar de un método público.La adopción 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, fue mejor marcar tipos con constructores públicos con[<AllowNullLiteral>]
y verificar args para nulos. No es peor que C # a este respecto.Las uniones discriminadas generalmente se compilan en jerarquías de clases, pero siempre tienen una representación relativamente amigable en C #. Yo diría, márquelos con
[<AllowNullLiteral>]
y úselos.Las funciones curry producen valores de función , que no deberían usarse.
Descubrí que era mejor adoptar null que depender de que se detectara en la interfaz pública e ignorarlo internamente. YMMV.
Tiene mucho sentido usar
list
/map
/set
internamente. Todos pueden exponerse a través de la interfaz pública comoIEnumerable<_>
. También,seq
,dict
, ySeq.readonly
son con frecuencia útiles.Ver # 6.
La estrategia que tome dependerá del tipo y tamaño de su biblioteca, pero, en mi experiencia, encontrar el punto óptimo entre F # y C # requirió menos trabajo, a largo plazo, que crear API independientes.
fuente
module Foo.Bar.Zoo
entonces en C #, esto estaráclass Foo
en el espacio de nombresFoo.Bar
. Sin embargo, si tiene módulos anidados comomodule Foo=... module Bar=... module Zoo==
, esto generará una claseFoo
con clases internasBar
yZoo
. ReIEnumerable
, esto puede no ser aceptable cuando realmente necesitamos trabajar con mapas y conjuntos. Revaloresnull
yAllowNullLiteral
- una de las razones por las que me gusta F # es porque estoy cansado denull
C #, ¡realmente no querría contaminar todo con él de nuevo!Aunque probablemente sería una exageración, podría considerar escribir una aplicación usando Mono.Cecil (tiene un soporte increíble en la lista de correo) que automatizaría la conversión en el nivel de IL. Por ejemplo, si implementas tu ensamblado en F #, usando la API pública de estilo F #, la herramienta generará un contenedor compatible con C # sobre él.
Por ejemplo, en F # obviamente usaría
option<'T>
(None
, específicamente) en lugar de usarnull
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 eraSome x
, entonces retornax
, de lo contrario retornanull
.Debería manejar el caso cuando
T
es un tipo de valor, es decir, no anulable; tendría que ajustar el valor de retorno del método contenedorNullable<T>
, lo que lo hace un poco doloroso.Nuevamente, estoy bastante seguro de que valdría la pena escribir una herramienta de este tipo en su escenario, tal vez excepto si trabajará en esta biblioteca de este tipo (que se puede usar sin problemas desde F # y C #) con regularidad. En cualquier caso, creo que sería un experimento interesante, uno que incluso podría explorar en algún momento.
fuente
Mono.Cecil
significaría básicamente escribir un programa para cargar el ensamblado de F #, encontrar elementos que requieran mapeo y emitir otro ensamblado con los contenedores de C #? ¿Hice esto bien?Borrador de las directrices de diseño de componentes F # (agosto de 2010)
fuente