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 #