Estou tentando criar uma biblioteca em F #. A biblioteca deve ser amigável para uso em F # e C # .
E é aqui que estou um pouco preso. Posso torná-lo compatível com F # ou C #, mas o problema é como torná-lo amigável para ambos.
Aqui está um exemplo. Imagine que tenho a seguinte função em F #:
let compose (f: 'T -> 'TResult) (a : 'TResult -> unit) = f >> a
Isso é perfeitamente utilizável no F #:
let useComposeInFsharp() =
let composite = compose (fun item -> item.ToString) (fun item -> printfn "%A" item)
composite "foo"
composite "bar"
Em C #, a compose
função possui a seguinte assinatura:
FSharpFunc<T, Unit> compose<T, TResult>(FSharpFunc<T, TResult> f, FSharpFunc<TResult, Unit> a);
Mas claro, eu não quero FSharpFunc
na assinatura, o que eu quero é Func
e ao Action
invés disso, assim:
Action<T> compose2<T, TResult>(Func<T, TResult> f, Action<TResult> a);
Para conseguir isso, posso criar compose2
funções como esta:
let compose2 (f: Func<'T, 'TResult>) (a : Action<'TResult> ) =
new Action<'T>(f.Invoke >> a.Invoke)
Agora, isso é perfeitamente utilizável em C #:
void UseCompose2FromCs()
{
compose2((string s) => s.ToUpper(), Console.WriteLine);
}
Mas agora temos um problema com o compose2
F #! Agora eu tenho que envolver todo o F # padrão funs
em Func
e Action
, assim:
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"
A pergunta: como podemos obter uma experiência de primeira classe com a biblioteca escrita em F # para usuários de F # e C #?
Até agora, não consegui pensar em nada melhor do que essas duas abordagens:
- Dois assemblies separados: um voltado para usuários F # e o segundo para usuários C #.
- Um assembly, mas namespaces diferentes: um para usuários F # e o segundo para usuários C #.
Para a primeira abordagem, eu faria algo assim:
Crie um projeto F #, chame-o de FooBarFs e compile-o em FooBarFs.dll.
- Direcione a biblioteca exclusivamente para usuários F #.
- Oculte tudo o que for desnecessário dos arquivos .fsi.
Crie outro projeto F #, chame se FooBarCs e compile-o em FooFar.dll
- Reutilize o primeiro projeto F # no nível de origem.
- Crie um arquivo .fsi que esconde tudo daquele projeto.
- Crie um arquivo .fsi que expõe a biblioteca em C #, usando expressões idiomáticas C # para nome, namespaces, etc.
- Crie wrappers que delegam à biblioteca central, fazendo a conversão quando necessário.
Acho que a segunda abordagem com os namespaces pode ser confusa para os usuários, mas você tem um assembly.
A pergunta: nenhum desses é ideal, talvez eu esteja faltando algum tipo de sinalizador / switch / atributo do compilador ou algum tipo de truque e há uma maneira melhor de fazer isso?
A questão: mais alguém tentou alcançar algo semelhante e, em caso afirmativo, como você o fez?
EDITAR: para esclarecer, a questão não é apenas sobre funções e delegados, mas a experiência geral de um usuário C # com uma biblioteca F #. Isso inclui namespaces, convenções de nomenclatura, expressões idiomáticas e semelhantes que são nativos do C #. Basicamente, um usuário C # não deve ser capaz de detectar que a biblioteca foi criada em F #. E vice-versa, um usuário F # deve sentir vontade de lidar com uma biblioteca C #.
EDIT 2:
Eu posso ver pelas respostas e comentários até agora que minha pergunta não tem a profundidade necessária, talvez principalmente devido ao uso de apenas um exemplo em que surgem problemas de interoperabilidade entre F # e C #, a questão dos valores de função. Acho que este é o exemplo mais óbvio e isso me levou a usá-lo para fazer a pergunta, mas da mesma forma deu a impressão de que essa é a única questão que me preocupa.
Deixe-me fornecer exemplos mais concretos. Eu li o mais excelente documento F # Component Design Guidelines (muito obrigado @gradbot por isso!). As diretrizes do documento, se utilizadas, tratam de alguns dos problemas, mas não de todos.
O documento está dividido em duas partes principais: 1) diretrizes para direcionar os usuários do F #; e 2) diretrizes para direcionar usuários C #. Em nenhum lugar ele tenta fingir que é possível ter uma abordagem uniforme, o que ecoa exatamente minha pergunta: podemos direcionar F #, podemos direcionar C #, mas qual é a solução prática para direcionar ambos?
Para relembrar, o objetivo é ter uma biblioteca criada em F #, e que possa ser usada linguisticamente nas linguagens F # e C #.
A palavra-chave aqui é idiomática . O problema não é a interoperabilidade geral, onde apenas é possível usar bibliotecas em diferentes linguagens.
Agora, para os exemplos, que peguei direto das Diretrizes de design de componentes do F # .
Módulos + funções (F #) vs Namespaces + Tipos + funções
F #: Use namespaces ou módulos para conter seus tipos e módulos. O uso idiomático é colocar funções em módulos, por exemplo:
// library module Foo let bar() = ... let zoo() = ... // Use from F# open Foo bar() zoo()
C #: Use namespaces, tipos e membros como a estrutura organizacional primária para seus componentes (ao contrário de módulos), para APIs .NET básicas.
Isso é incompatível com a diretriz F # e o exemplo precisaria ser reescrito para se adequar aos usuários C #:
[<AbstractClass; Sealed>] type Foo = static member bar() = ... static member zoo() = ...
Ao fazer isso, porém, quebramos o uso idiomático do F # porque não podemos mais usar
bar
ezoo
sem prefixá-lo comFoo
.
Uso de tuplas
F #: Use tuplas quando apropriado para valores de retorno.
C #: Evite usar tuplas como valores de retorno em APIs .NET do vanilla.
Assíncrono
F #: Use Async para programação assíncrona nos limites da API F #.
C #: Exponha operações assíncronas usando o modelo de programação assíncrona .NET (BeginFoo, EndFoo) ou como métodos que retornam tarefas .NET (Tarefa), em vez de objetos F # Async.
Uso de
Option
F #: Considere usar valores de opção para tipos de retorno em vez de gerar exceções (para código voltado para F #).
Considere usar o padrão TryGetValue em vez de retornar valores de opção F # (opção) em APIs .NET vanilla e prefira a sobrecarga de método em vez de usar valores de opção F # como argumentos.
Sindicatos discriminados
F #: Use uniões discriminadas como alternativa às hierarquias de classe para criar dados estruturados em árvore
C #: não há diretrizes específicas para isso, mas o conceito de sindicatos discriminados é estranho ao C #
Funções de curry
F #: funções curried são idiomáticas para F #
C #: Não use currying de parâmetros em APIs .NET do vanilla.
Verificando valores nulos
F #: isso não é idiomático para F #
C #: Considere verificar se há valores nulos nos limites da API .NET vanilla.
Uso de tipos de F #
list
,map
,set
, etcF #: é idiomático usá-los em F #
C #: Considere usar os tipos de interface de coleção .NET IEnumerable e IDictionary para parâmetros e valores de retorno em APIs .NET vanilla. ( Ou seja, não usar F #
list
,map
,set
)
Tipos de função (o óbvio)
F #: o uso de funções F # como valores é idiomático para F #, obviamente
C #: Use tipos de delegado .NET em preferência aos tipos de função F # em APIs .NET vanilla.
Acho que isso deve ser suficiente para demonstrar a natureza da minha pergunta.
A propósito, as diretrizes também têm uma resposta parcial:
... uma estratégia de implementação comum ao desenvolver métodos de ordem superior para bibliotecas .NET vanilla é criar toda a implementação usando tipos de função F # e, em seguida, criar a API pública usando delegados como uma fachada fina sobre a implementação real do F #.
Para resumir.
Há uma resposta definitiva: não há truques do compilador que eu perdi .
De acordo com o documento de diretrizes, parece que criar primeiro para F # e depois criar um invólucro de fachada para .NET é uma estratégia razoável.
A questão então permanece em relação à implementação prática disso:
Conjuntos separados? ou
Namespaces diferentes?
Se minha interpretação estiver correta, Tomas sugere que usar namespaces separados deve ser suficiente e deve ser uma solução aceitável.
Acho que vou concordar com isso, visto que a escolha de namespaces é tal que não surpreende ou confunde os usuários .NET / C #, o que significa que o namespace para eles provavelmente deve parecer o seu principal. Os usuários do F # terão que assumir o encargo de escolher o namespace específico do F #. Por exemplo:
FSharp.Foo.Bar -> namespace para F # voltado para a biblioteca
Foo.Bar -> namespace para .NET wrapper, idiomático para C #