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 #
Respostas:
Daniel já explicou como definir uma versão amigável do C # da função F # que você escreveu, portanto, acrescentarei alguns comentários de nível superior. Em primeiro lugar, você deve ler as Diretrizes de design de componentes F # (já referenciadas pelo gradbot). Este é um documento que explica como projetar bibliotecas F # e .NET usando F # e deve responder a muitas de suas perguntas.
Ao usar F #, existem basicamente dois tipos de bibliotecas que você pode escrever:
A biblioteca F # foi projetada para ser usada apenas a partir do F #, então sua interface pública é escrita em um estilo funcional (usando tipos de função F #, tuplas, uniões discriminadas, etc.)
A biblioteca .NET foi projetada para ser usada em qualquer linguagem .NET (incluindo C # e F #) e normalmente segue o estilo orientado a objetos .NET. Isso significa que você vai expor a maior parte da funcionalidade como classes com método (e às vezes métodos de extensão ou métodos estáticos, mas principalmente o código deve ser escrito no design OO).
Em sua pergunta, você está perguntando como expor a composição de funções como uma biblioteca .NET, mas acho que funções como o seu
compose
são conceitos de nível muito baixo do ponto de vista da biblioteca .NET. Você pode expô-los como métodos que trabalham comFunc
eAction
, mas provavelmente não é assim que você projetaria uma biblioteca .NET normal em primeiro lugar (talvez você usasse o padrão Builder em vez disso ou algo parecido).Em alguns casos (ou seja, ao projetar bibliotecas numéricas que não se adaptam bem ao estilo de biblioteca .NET), faz sentido projetar uma biblioteca que combine os estilos F # e .NET em uma única biblioteca. A melhor maneira de fazer isso é ter a API F # normal (ou .NET normal) e, em seguida, fornecer invólucros para uso natural no outro estilo. Os wrappers podem estar em um namespace separado (como
MyLibrary.FSharp
eMyLibrary
).Em seu exemplo, você poderia deixar a implementação do F #
MyLibrary.FSharp
e adicionar wrappers .NET (C # -friendly) (semelhante ao código que Daniel postou) noMyLibrary
namespace como método estático de alguma classe. Mas, novamente, a biblioteca .NET provavelmente teria API mais específica do que composição de função.fonte
Você só precisa agrupar os valores da função ( funções parcialmente aplicadas, etc) com
Func
ouAction
, o resto é convertido automaticamente. Por exemplo:Portanto, faz sentido usar
Func
/Action
quando aplicável. Isso elimina suas preocupações? Acho que as soluções propostas são excessivamente complicadas. Você pode escrever sua biblioteca inteira em F # e usá-la sem problemas a partir de F # e C # (eu faço isso o tempo todo).Além disso, F # é mais flexível do que C # em termos de interoperabilidade, portanto, geralmente é melhor seguir o estilo .NET tradicional quando isso for uma preocupação.
EDITAR
O trabalho necessário para fazer duas interfaces públicas em namespaces separados, eu acho, só é garantido quando elas são complementares ou a funcionalidade F # não pode ser usada em C # (como funções embutidas, que dependem de metadados específicos do F #).
Resumindo seus pontos:
Módulo +
let
ligações e membros do tipo sem construtor + estáticos aparecem exatamente da mesma forma em C #, então vá com módulos, se puder. Você pode usarCompiledNameAttribute
para dar aos membros nomes amigáveis ao C #.Posso estar errado, mas meu palpite é que as Diretrizes de Componentes foram escritas antes de
System.Tuple
serem adicionadas à estrutura. (Em versões anteriores, F # definia seu próprio tipo de tupla.) Desde então, tornou-se mais aceitável usarTuple
em uma interface pública para tipos triviais.É aqui que eu acho que você deve fazer as coisas da maneira C # porque F # funciona bem com,
Task
mas C # não funciona bem comAsync
. Você pode usar async internamente e depois chamarAsync.StartAsTask
antes de retornar de um método público.A adoção de
null
pode ser a maior desvantagem ao desenvolver uma API para uso em C #. No passado, tentei todos os tipos de truques para evitar considerar nulo no código F # interno, mas, no final, era melhor marcar tipos com construtores públicos com[<AllowNullLiteral>]
e verificar args para nulos. Não é pior do que C # nesse aspecto.As uniões discriminadas são geralmente compiladas para hierarquias de classes, mas sempre têm uma representação relativamente amigável em C #. Eu diria, marque-os
[<AllowNullLiteral>]
e use-os.Funções curried produzem valores de função , que não devem ser usados.
Achei melhor abraçar o null do que depender de ele ser capturado na interface pública e ignorá-lo internamente. YMMV.
Faz muito sentido usar
list
/map
/set
internamente. Todos eles podem ser expostos por meio da interface pública comoIEnumerable<_>
. Além disso,seq
,dict
, eSeq.readonly
são freqüentemente úteis.Veja # 6.
A estratégia que você adota depende do tipo e tamanho da sua biblioteca, mas, em minha experiência, encontrar o ponto ideal entre F # e C # exigiu menos trabalho - a longo prazo - do que criar APIs separadas.
fonte
module Foo.Bar.Zoo
em C #, seráclass Foo
no namespaceFoo.Bar
. No entanto, se você tiver módulos aninhados comomodule Foo=... module Bar=... module Zoo==
, isso irá gerar uma classeFoo
com classes internasBar
eZoo
. ReIEnumerable
, isso pode não ser aceitável quando realmente precisamos trabalhar com mapas e conjuntos. Renull
valores eAllowNullLiteral
- uma razão que eu como F # é porque eu estou cansado denull
em C #, realmente não gostaria de tudo poluir com ele novamente!Embora provavelmente seja um exagero, você pode considerar escrever um aplicativo usando Mono.Cecil (ele tem um suporte incrível na lista de discussão) que automatizaria a conversão no nível IL. Por exemplo, se você implementar seu assembly em F #, usando a API pública no estilo F #, a ferramenta gerará um wrapper amigável C # sobre ele.
Por exemplo, em F # você obviamente usaria
option<'T>
(None
, especificamente) em vez de usarnull
como em C #. Escrever um gerador de wrapper para este cenário deve ser bastante fácil: o método de wrapper invocaria o método original: se seu valor de retorno fosseSome x
, então retornex
, caso contrário, retornarianull
.Você precisaria lidar com o caso em que
T
é um tipo de valor, ou seja, não anulável; você teria que envolver o valor de retorno do método wrapper emNullable<T>
, o que o torna um pouco complicado.Mais uma vez, estou certo de que valeria a pena escrever tal ferramenta em seu cenário, talvez exceto se você estiver trabalhando nesta biblioteca (utilizável perfeitamente em F # e C #) regularmente. Em todo caso, acho que seria uma experiência interessante, que poderia até explorar algum dia.
fonte
Mono.Cecil
significaria basicamente escrever um programa para carregar assembly F #, localizar itens que requerem mapeamento e emitir outro assembly com os wrappers C #? Eu entendi direito?Rascunho das diretrizes de design de componentes F # (agosto de 2010)
fonte