Sto cercando di progettare una libreria in F #. La libreria dovrebbe essere facile da usare sia da F # che da C # .
Ed è qui che sono bloccato un po '. Posso renderlo compatibile con F # o renderlo compatibile con C #, ma il problema è come renderlo amichevole per entrambi.
Ecco un esempio. Immagina di avere la seguente funzione in F #:
let compose (f: 'T -> 'TResult) (a : 'TResult -> unit) = f >> a
Questo è perfettamente utilizzabile da F #:
let useComposeInFsharp() =
let composite = compose (fun item -> item.ToString) (fun item -> printfn "%A" item)
composite "foo"
composite "bar"
In C #, la compose
funzione ha la seguente firma:
FSharpFunc<T, Unit> compose<T, TResult>(FSharpFunc<T, TResult> f, FSharpFunc<TResult, Unit> a);
Ma ovviamente, non voglio FSharpFunc
nella firma, quello che voglio è Func
e Action
invece, in questo modo:
Action<T> compose2<T, TResult>(Func<T, TResult> f, Action<TResult> a);
Per ottenere ciò, posso creare compose2
funzioni come questa:
let compose2 (f: Func<'T, 'TResult>) (a : Action<'TResult> ) =
new Action<'T>(f.Invoke >> a.Invoke)
Ora, questo è perfettamente utilizzabile in C #:
void UseCompose2FromCs()
{
compose2((string s) => s.ToUpper(), Console.WriteLine);
}
Ma ora abbiamo un problema usando compose2
da F #! Ora devo racchiudere tutto lo standard F # funs
in Func
e Action
, in questo modo:
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 domanda: come possiamo ottenere un'esperienza di prima classe per la libreria scritta in F # sia per gli utenti F # che per quelli C #?
Finora, non sono riuscito a trovare niente di meglio di questi due approcci:
- Due assembly separati: uno destinato agli utenti di F # e il secondo agli utenti di C #.
- Un assembly ma spazi dei nomi diversi: uno per gli utenti di F # e il secondo per gli utenti di C #.
Per il primo approccio, farei qualcosa del genere:
Creare un progetto F #, chiamarlo FooBarFs e compilarlo in FooBarFs.dll.
- Indirizza la libreria esclusivamente agli utenti F #.
- Nascondi tutto ciò che non è necessario dai file .fsi.
Creare un altro progetto F #, chiamare if FooBarCs e compilarlo in FooFar.dll
- Riutilizzare il primo progetto F # a livello di origine.
- Crea file .fsi che nasconde tutto da quel progetto.
- Crea un file .fsi che esponga la libreria in modo C #, utilizzando idiomi C # per nome, spazi dei nomi, ecc.
- Crea wrapper che delegano alla libreria principale, eseguendo la conversione dove necessario.
Penso che il secondo approccio con gli spazi dei nomi possa creare confusione per gli utenti, ma poi hai un assembly.
La domanda: nessuno di questi è l'ideale, forse mi manca qualche tipo di flag / switch / attributo del compilatore o qualche tipo di trucco e c'è un modo migliore per farlo?
La domanda: qualcun altro ha provato a realizzare qualcosa di simile e se sì come hai fatto?
EDIT: per chiarire, la domanda non riguarda solo funzioni e delegati, ma l'esperienza complessiva di un utente C # con una libreria F #. Ciò include spazi dei nomi, convenzioni di denominazione, espressioni idiomatiche e simili che sono nativi di C #. Fondamentalmente, un utente C # non dovrebbe essere in grado di rilevare che la libreria è stata creata in F #. E viceversa, un utente F # dovrebbe avere la sensazione di avere a che fare con una libreria C #.
MODIFICA 2:
Posso vedere dalle risposte e dai commenti finora che la mia domanda manca della profondità necessaria, forse principalmente a causa dell'uso di un solo esempio in cui sorgono problemi di interoperabilità tra F # e C #, la questione dei valori delle funzioni. Penso che questo sia l'esempio più ovvio e quindi questo mi ha portato a usarlo per porre la domanda, ma per lo stesso motivo ha dato l'impressione che questo sia l'unico problema che mi interessa.
Consentitemi di fornire esempi più concreti. Ho letto il documento F # Component Design Guidelines più eccellente (molte grazie @gradbot per questo!). Le linee guida nel documento, se utilizzate, affrontano alcuni dei problemi ma non tutti.
Il documento è suddiviso in due parti principali: 1) linee guida per il targeting degli utenti F #; e 2) linee guida per il targeting degli utenti C #. Da nessuna parte tenta nemmeno di fingere che sia possibile avere un approccio uniforme, che riecheggia esattamente la mia domanda: possiamo targetizzare F #, possiamo targetizzare C #, ma qual è la soluzione pratica per targetizzare entrambi?
Per ricordare, l'obiettivo è avere una libreria creata in F # e che può essere utilizzata in modo idiomatico da entrambi i linguaggi F # e C #.
La parola chiave qui è idiomatica . Il problema non è l'interoperabilità generale in cui è solo possibile utilizzare librerie in lingue diverse.
Passiamo ora agli esempi, che prendo direttamente dalle linee guida per la progettazione di componenti F # .
Moduli + funzioni (F #) vs spazi dei nomi + tipi + funzioni
F #: usa spazi dei nomi o moduli per contenere i tuoi tipi e moduli. L'uso idiomatico è inserire funzioni nei moduli, ad esempio:
// library module Foo let bar() = ... let zoo() = ... // Use from F# open Foo bar() zoo()
C #: usa spazi dei nomi, tipi e membri come struttura organizzativa principale per i tuoi componenti (al contrario dei moduli), per le API .NET vanilla.
Ciò non è compatibile con le linee guida di F # e l'esempio dovrebbe essere riscritto per adattarsi agli utenti C #:
[<AbstractClass; Sealed>] type Foo = static member bar() = ... static member zoo() = ...
In questo modo, tuttavia, interrompiamo l'uso idiomatico di F # perché non possiamo più usarlo
bar
ezoo
senza prefissarlo conFoo
.
Uso di tuple
F #: usa le tuple quando appropriato per i valori restituiti.
C #: evitare di usare tuple come valori restituiti nelle API .NET vanilla.
Async
F #: usa Async per la programmazione asincrona ai limiti dell'API F #.
C #: esporre operazioni asincrone utilizzando il modello di programmazione asincrona .NET (BeginFoo, EndFoo) o come metodi che restituiscono attività .NET (attività), anziché come oggetti asincroni F #.
Uso di
Option
F #: prendere in considerazione l'utilizzo di valori di opzione per i tipi restituiti invece di generare eccezioni (per il codice di affiancamento F #).
Prendi in considerazione l'utilizzo del pattern TryGetValue invece di restituire i valori dell'opzione F # (opzione) nelle API .NET vanilla e preferisci il sovraccarico del metodo rispetto all'assunzione dei valori dell'opzione F # come argomenti.
Unioni discriminate
F #: utilizzare unioni discriminate come alternativa alle gerarchie di classi per la creazione di dati strutturati ad albero
C #: nessuna linea guida specifica per questo, ma il concetto di unioni discriminate è estraneo a C #
Funzioni al curry
F #: le funzioni al curry sono idiomatiche per F #
C #: non utilizzare il currying dei parametri nelle API .NET vanilla.
Verifica di valori nulli
F #: questo non è idiomatico per F #
C #: valutare la possibilità di verificare la presenza di valori null sui limiti dell'API .NET vanilla.
L'utilizzo di F # tipi
list
,map
,set
, eccF #: è idiomatico usarli in F #
C #: prendere in considerazione l'utilizzo dei tipi di interfaccia della raccolta .NET IEnumerable e IDictionary per i parametri e i valori restituiti nelle API .NET vanilla. ( Cioè non usare F #
list
,map
,set
)
Tipi di funzione (quella ovvia)
F #: l' uso delle funzioni F # come valori è idiomatico per F #, ovviamente
C #: utilizzare i tipi delegati .NET rispetto ai tipi di funzione F # nelle API .NET vanilla.
Penso che dovrebbero essere sufficienti per dimostrare la natura della mia domanda.
Per inciso, le linee guida hanno anche una risposta parziale:
... una strategia di implementazione comune quando si sviluppano metodi di ordine superiore per le librerie .NET vanilla è quella di creare tutta l'implementazione utilizzando i tipi di funzione F #, quindi creare l'API pubblica utilizzando i delegati come una sottile facciata sopra l'implementazione F # effettiva.
Riassumere.
C'è una risposta definitiva: non ci sono trucchi del compilatore che mi sono perso .
Secondo il documento delle linee guida, sembra che creare prima per F # e poi creare un involucro di facciata per .NET sia una strategia ragionevole.
La domanda rimane quindi per quanto riguarda l'attuazione pratica di questo:
Assemblee separate? o
Spazi dei nomi diversi?
Se la mia interpretazione è corretta, Tomas suggerisce che l'uso di spazi dei nomi separati dovrebbe essere sufficiente e dovrebbe essere una soluzione accettabile.
Penso di essere d'accordo con questo dato che la scelta degli spazi dei nomi è tale da non sorprendere o confondere gli utenti .NET / C #, il che significa che lo spazio dei nomi per loro dovrebbe probabilmente apparire come lo spazio dei nomi principale per loro. Gli utenti di F # dovranno assumersi l'onere di scegliere lo spazio dei nomi specifico di F #. Per esempio:
FSharp.Foo.Bar -> spazio dei nomi per F # rivolto verso la libreria
Foo.Bar -> spazio dei nomi per .NET wrapper, idiomatico per C #