Este problema é uma discussão sobre o suporte Python para Fable. O Babel AST usado atualmente pelo Fable é próximo o suficiente para gerar código Python. Especialmente agora que as aulas são suportadas.
Um POC foi feito para mostrar que isso é possível. No entanto, é necessário muito trabalho para:
A biblioteca Expression permite a programação funcional em Python inspirada em F #. Ele fornece sua própria implementação de tipos de dados como option
, result
, seq
, map
, list
(chamado FrozenList), caixa de correio processador, ... Portanto, deve ser adequado como o equivalente Python da biblioteca Fable. Em Python, você usa Expression para fazer código ish F # '. No Fable, você gera código Python usando Expression.
int
tem um comprimento arbitrário. Devemos emular tipos .NET (máquina) para serem compatíveis ou devemos expor o tipo Python int
como um int F #?O código-fonte do POC reside atualmente aqui:
brew install [email protected]
no Mac. Observe que python pode estar disponível em seu sistema como python
, python3
ou ambos.python
.dotnet fsi build.fsx library-py
dotnet fsi build.fsx test-py
QuickTest.fs
para algum código F # muito simples. Em seguida, execute dotnet fsi build.fsx quicktest
do diretório superior.Agora você pode editar os códigos Fable e Expression e o código de exemplo será recompilado quando necessário. Observe que você precisa de um salvamento adicional de QuickTest.fs
para acionar a recompilação após modificar o código do Fable. O arquivo python estará disponível como quicktest.py
. Uma boa demonstração é visualizar F #, JS e Python no vscode ao mesmo tempo. Em seguida, você verá como o código está sendo transformado ao salvar o arquivo F #:
Você pode executar o código Python gerado no terminal:
$ python3 quicktest.py
Alguns links relevantes:
Sinta-se à vontade para contribuir com discussões, comentários, ideias e código 😍
Este é um ótimo trabalho @dbrattli! 👏 👏 👏 Minha ideia original com o Fable era fornecer uma maneira de compilar facilmente o F # para outras linguagens. Para o bem ou para o mal, o Fable evoluiu com JS como seu foco principal, então seria necessário algum trabalho para torná-lo mais agnóstico quanto ao idioma, mas eu estaria aberto a isso. Alguns comentários às suas perguntas:
Repo igual ou diferente? Eu seria a favor de manter o máximo de código comum possível. Usar repositórios diferentes provavelmente os fará divergir bastante rápido (já é difícil manter em sincronia o branch com o suporte de cotação), mas também precisamos tornar o código modular o suficiente para que não tenhamos condições em todos os lugares, dependendo do idioma de destino. Uma solução intermediária seria tentar extrair o máximo de código possível para um "Núcleo" da Fable (não podemos usar o Core 😉) e então ter diferentes repositórios para as implementações da linguagem.
Babel AST: Isso foi usado porque originalmente estávamos contando com o Babel para imprimir o código. Em um mundo ideal, agora que temos nossa impressora, deve ser possível removê-la. Mas há muito trabalho acontecendo na etapa Fable-to-Babel, então seria difícil. Pelo mesmo motivo, seria preferível reutilizar o máximo de código possível a partir desta etapa, talvez tornando o Babel AST algo mais genérico para linguagens não funcionais. Alguns exemplos:
Tornar o F # mais pitônico? Com o Fable, sempre tentei encontrar um equilíbrio em que a semântica do .NET fosse sacrificada às vezes para evitar sobrecarga. Por exemplo, usamos apenas o número JS em vez de ter um tipo específico para ints (embora façamos isso para longs), isso se aplicaria ao Python int
, presumo. No entanto, muitas contribuições foram feitas para respeitar a semântica do .NET em pontos específicos para evitar resultados inesperados, por exemplo, em divisões de inteiros ou conversões numéricas explícitas.
De qualquer forma, quando você diz "mais pythônico", você quer dizer estilo de código ou APIs nativas? O Fable não tenta suportar a maior parte do .NET BCL, mas no final precisamos suportar a função / operadores comuns porque é a isso que os desenvolvedores estão acostumados (mesmo os novatos, porque isso eles encontrariam em tutoriais). É bom dar a opção de usar as APIs nativas em qualquer caso ( Fable.Extras é uma boa jogada nesse sentido). Por exemplo, JS.console.log
geralmente formata objetos mais bem do que printfn
, mas precisamos oferecer suporte a este último porque é o que os desenvolvedores mais usam.
Eu amo essa ideia Estou especialmente ansioso para usar bibliotecas Python em F #. Idealmente, devemos ter a opção de consumir bibliotecas Python puras ou SciSharp - elas tendem a ter API quase exata como Python (o mesmo código F # executado no contexto Python ou .NET!)
Esse código F # pode ser compilado com Fable em Python e, em seguida, usado nos kernels Notebooks (Jupyter ou .NET Interactive), tornando o fluxo de trabalho analítico um recurso real orientado para o domínio (eu poderia considerar tudo como um "recurso matador").
.NET Interactive é muito extensível, rodar o Fable dentro do F # Kernel é muito fácil (eu tenho PoC funcionando).
Outra coisa é que prevejo que o MS revelará algo semelhante em breve para todo o .NET (a previsão é baseada apenas nas atividades e objetivos do .NET Interactive abordar, então posso estar errado). Mesmo que o façam, ainda queremos que o Fable o tenha separadamente para ter o arsenal JS / F # / Python completo em um mundo funcional. A abordagem MS seria mais como um sabor Blazor (eu acho).
Obrigado, este é um ótimo feedback @alfonsogarciacaro , @PawelStadnicki . O que eu quis dizer é se queremos deixar o programador ter a sensação de "Python" expondo mais da biblioteca padrão do
Acho que poderíamos ter o melhor de ambos os mundos e deixar o usuário decidir o que importar ou não (por exemplo, usar datetime
do Python ou DateTime
do .NET). Portanto, não há necessidade de decidir, apenas implementar o que mais queremos (sob demanda). Mas eu suspeito que o mesmo problema seja relevante para o Fable JS. Você sabe que está implementando um aplicativo da web e provavelmente não espera que todo o .NET esteja disponível. Você prefere ter melhor acesso ao ecossistema JS.
Portanto, não há necessidade de decidir antecipadamente. Vamos arregaçar as mangas e começar 😄 Será ótimo fazer parte do Fable e ter esse problema para fazer perguntas ao longo do caminho, por exemplo, sobre transformações etc. Há muito o que aprender.
PS: Expression btw também tem um decorador de chamada final que podemos usar (sincronizar e async). Aqui está um exemplo de uso em aiorreativo.
Me faz pensar em # 1601.
Eu me pergunto se há uma boa maneira de tornar o Fable poderoso o suficiente para que eles possam ser feitos como "plug-ins" de back-end de terceiros (pense em LLVM, mas específico para F #). Infinitas possibilidades.
Poderíamos tentar tornar o Babel AST mais genérico (e digitado) para que ele possa ser facilmente transformado / impresso em linguagens do tipo C. No entanto, se eu aprendi algo durante o desenvolvimento do Fable é que escrever um compilador simples de F # para outra linguagem é (mais ou menos) fácil, mas fornecendo uma boa experiência de desenvolvimento e integração, portanto, as vantagens do F # menos o atrito com o ecossistema estrangeiro pesa mais do que desenvolver na língua nativa é bastante difícil.
O que disse @alfonsogarciacaro . Parte desse desafio é migrar mais da implementação Fable BCL para F # para que não precise ser reescrito para todas as linguagens, enquanto mantém um desempenho nativo aceitável para o código gerado, algo que não é trivial mesmo com JavaScript.
Interessante. Não percebi que partes significativas da implementação do Fable BCL foram escritas em (presumivelmente) Javascript. Achei que já seria mais F #. Não duvido que haja um bom motivo para isso - por que acabou sendo mais fácil assim?
@jwosty A
Além disso, .NET BCL
tem uma superfície de API muito grande, além de FSharp.Core
, então é difícil manter esse esforço sem uma equipe maior (embora @alfonsogarciacaro de alguma forma consiga mantê-la e desenvolvê-la principalmente por si mesmo, que é incrível e louvável).
Acho que o que estou tentando dizer é que contribuições (e ideias) são muito bem-vindas e apreciadas.
A (não tão curta) resposta é: por motivos de desempenho e para fazer a ponte entre os tipos F # e os tipos nativamente suportados pelo navegador e para uma melhor integração do código gerado no ecossistema JavaScript.
Eu posso ver isso.
Acho que o que estou tentando dizer é que contribuições (e ideias) são muito bem-vindas e apreciadas.
Com certeza! Eu entendo o quanto esforço é necessário para manter projetos tão grandes e tão importantes quanto este, e eu realmente aprecio o suor de sangue e as lágrimas que vocês colocaram neles. Definitivamente estou mantendo meus olhos abertos para lugares para contribuir, como Fable ultimamente tem sido muito, muito útil para mim e eu adoraria retribuir.
Eu poderia usar um pouco de ajuda. No momento, estou usando o projeto QuickTest para desenvolvimento (ou seja, dotnet fsi build.fsx quicktest
). Como imprimo o Babel AST durante a revelação? ( Atualização : encontrei ASTViewer ).
Meu problema atual é que o Python não oferece suporte a lambda de várias linhas (funções de seta), portanto, o lambda precisa ser transformado e extraído para uma função nomeada e, em vez disso, chamar essa função. Quaisquer dicas aqui seriam apreciadas. Por exemplo:
let fn args cont =
cont args
let b = fn 10 (fun a ->
a + 1
)
Terá de ser transformado para:
let fn args cont =
cont args
let cont a =
a + 1
let b = fn 10 cont
Como devo pensar ao substituir uma expressão de chamada por várias instruções, uma vez que, ao transformar a chamada de função com args, preciso substituí-la em um nível superior ou usar algum tipo de instrução de bloco, por exemplo, if true then ...
Alguma idéia?
@dbrattli Veja também esta impressora , que também imprime algumas das propriedades.
Não percebi que partes significativas da implementação do Fable BCL foram escritas em (presumivelmente) Javascript. Achei que já seria mais F #. Não tenho dúvidas de que há um bom motivo para isso - por que ficou mais fácil assim?
Sim, como disse @ncave , o principal motivo foi gerar mais JS padrão. Por exemplo, em Funscript todas as substituições FSharp.Core foram escritas em F #, mas com Fable eu queria integrar com padrões JS quando possível (por exemplo, usando iteradores JS em vez de .NET IEnumerable), o que trouxe muitos problemas de ovo e galinha. foi mais fácil escrevê-lo em JS / Typescript no início. No início, também tive a ideia de publicar a biblioteca de fábulas como um pacote separado que poderia ser usado em projetos JS, mas rapidamente descartei.
No Fable 2, começamos um esforço para portar alguns dos módulos para F #. Isso tem sido muito benéfico porque aumenta o dogfood e nos permite reutilizar facilmente as melhorias no FSharp.Core (como com mapas e conjuntos recentemente). No entanto, ainda existem alguns hacks aqui e ali para contornar os problemas do ovo e da galinha. E não vamos falar sobre o grande monstro que é o módulo Replacements :)
Além disso, como @ncave diz, manter esta biblioteca é um grande empreendimento, é por isso que estou sempre relutante em aumentar o suporte para BCL para reduzir a superfície de manutenção, mas tive muita sorte de ter havido várias contribuições excelentes para a biblioteca Desde o ínicio.
@dbrattli Sobre a impressão do AST, já faz um tempo que não uso a ferramenta ASTViewer no repositório do Fable. As melhores opções são aquelas apontadas por visualizador Fantomas que é o que eu uso recentemente quando preciso inspecionar o F # AST (apenas certifique-se de selecionar Mostrar AST digitado ). Infelizmente, ainda não temos impressoras específicas para Fable ou Bable AST. No caso do Fable AST, como é apenas uniões e registros, um simples printfn "%A"
funciona, embora a informação de localização seja um pouco barulhenta.
Sobre como extrair os lambdas. É possível, mas requer algum trabalho. Acho que podemos fazer algo semelhante às importações, ou seja, coletá-las ao percorrer o AST e atribuir a elas um identificador exclusivo. Posteriormente, podemos apenas declarar as funções no início do arquivo . O principal desafio provavelmente serão os valores capturados, não me lembro se o F # AST tem informações sobre isso, mas mesmo que tenha, infelizmente não os mantemos no AST, então acho que teremos que encontre todos os idents usados no corpo do lambda que não correspondem aos argumentos (ou ligações dentro do lambda) e converta-os em argumentos extras do lambda (e eu acho que eles devem ir no início para evitar interferir no currying).
Obrigado pela ótima informação @alfonsogarciacaro , meu primeiro PoC estava apenas alterando Babel.fs
/ Fable2Babel.fs
, BabelPrinter.fs
etc. Agora comecei do zero com um Python AST separado, ou seja, Python.fs
, PythonPrinter.fs
. Então tentarei adicionar Babel2Python.fs
para transformar de Babel AST em Python AST, pois isso pode ser mais fácil do que converter de Fable AST (mas também possível com Fable2Python.fs
se a conversão de Babel acabar sendo difícil). Dessa forma, não tocarei muito na base de código existente e terei minha própria transformação, onde tentarei fazer a reescrita do lambda.
Isso faz sentido. Uma dificuldade é que o Babel AST não é feito de DUs, então é um pouco mais difícil atravessá-lo com o casamento de padrões. Talvez # 2158 possa ajudar?
@alfonsogarciacaro Acho que consegui consertar a reescrita das funções de setas e expressões de funções de uma forma que acho (espero) que realmente funcione. A ideia é que cada expressão ( TransformAsExpr
) também retorne uma lista de declarações (que precisa ser concatenada e passada até a última lista de declarações (nível da última declaração). declarações retornadas (func-def) serão levantadas e escritas na frente de outras declarações. Portanto, uma seta ou expressão de função simplesmente retornará um name-expression, [statement ]
onde a seta / expressão de função é reescrita para um declaração e retornada junto com a expressão de nome. Isso significa que:
module QuickTest
let fn args cont =
cont args
let b = fn 10 (fun a ->
printfn "test"
a + 1
)
Gera o seguinte JS:
import { printf, toConsole } from "./.fable/fable-library.3.1.1/String.js";
export function fn(args, cont) {
return cont(args);
}
export const b = fn(10, (a) => {
toConsole(printf("test"));
return (a + 1) | 0;
});
O que, por sua vez, gera o seguinte Python:
from expression.fable.string import (printf, toConsole)
def fn(args, cont):
return cont(args)
def lifted_5094(a):
toConsole(printf("test"))
return (a + 1) | 0
b = fn(10, lifted_5094)
Achei que poderia ter problemas para lidar com fechamentos, mas na maioria (todos?) Dos casos, os fechamentos também são levantados, por exemplo:
module QuickTest
let add(a, b, cont) =
cont(a + b)
let square(x, cont) =
cont(x * x)
let sqrt(x, cont) =
cont(sqrt(x))
let pythagoras(a, b, cont) =
square(a, (fun aa ->
printfn "1"
square(b, (fun bb ->
printfn "2"
add(aa, bb, (fun aabb ->
printfn "3"
sqrt(aabb, (fun result ->
cont(result)
))
))
))
))
Será reescrito para:
from expression.fable.string import (printf, toConsole)
def add(a, b, cont):
return cont(a + b)
def square(x, cont):
return cont(x * x)
def sqrt(x, cont):
return cont(math.sqrt(x))
def pythagoras(a, b, cont):
def lifted_1569(aa):
toConsole(printf("1"))
def lifted_790(bb):
toConsole(printf("2"))
def lifted_6359(aabb):
toConsole(printf("3"))
return sqrt(aabb, lambda result: cont(result))
return add(aa, bb, lifted_6359)
return square(b, lifted_790)
return square(a, lifted_1569)
De qualquer forma, é um bom começo 😄
Fiz exatamente o mesmo para PHP (e está executando CrazyFarmers em BGA ), então talvez possamos combinar o esforço aqui?
@ncave Você btw tem alguma dica sobre como usar o Fable para uso interativo, como os cadernos Jupyter? O problema é que o Jupyter fornecerá um fragmento de código (célula) e o kernel precisa compilar esse fragmento junto com quaisquer declarações locais existentes de instruções anteriores.
Fable não é ideal para isso, mas criei uma PoC onde mantenho um dicionário ordenado de instruções / declarações anteriores (let, type, open) e crio um programa F # válido usando-as em ordem junto com o último fragmento de código. Todas as declarações no fragmento de código são removidas do dicionário antes da execução e adicionadas depois. Isso parece funcionar, mas estou me perguntando se existe talvez uma maneira melhor?
Fonte: https://github.com/dbrattli/Fable.Jupyter/blob/main/fable/kernel.py#L85
Então eu tenho um Fable cli em execução em segundo plano que recompila o Fable.fs sempre que for atualizado.
dotnet watch -p src/Fable.Cli run -- watch --cwd /Users/dbrattli/Developer/GitHub/Fable.Jupyter --exclude Fable.Core --forcePkgs --python
@dbrattli Parece razoável, se os fragmentos forem pequenos. Para os maiores, talvez usar um arquivo separado para cada célula (fragmento) pode acelerar as coisas, já que o Fable usa algum cache para pular o trabalho desnecessário em arquivos que não foram alterados.
Apenas para sua informação, se precisar de mais controle, você também pode usar o Fable como uma biblioteca , não apenas CLI (consulte fable-standalone
e um exemplo de uso).
1) Um problema que eu tenho é que o Fable AST não tem Throw / Raise. Portanto, isso será transformado em uma expressão do Emit que é difícil (er) para mim analisar. Ou pelo menos parece desnecessário quando Babel tem um ThrowStatement. Há um bom motivo para ele não estar sendo usado ou devo tentar consertá-lo? @alfonsogarciacaro
let divide1 x y =
try
Some (x / y)
with
| :? System.DivideByZeroException -> printfn "Division by zero!"; None
let result1 = divide1 100 0
Gera:
export function divide1(x, y) {
try {
return ~(~(x / y));
}
catch (matchValue) {
throw matchValue;
}
}
export const result1 = divide1(100, 0);
Onde throw matchValue
é um EmitExpression
.
2) Devemos adicionar ThisExpr
ao Fable AST, para que eu possa identificar se this
está sendo usado como esta palavra-chave. Eu não posso ter certeza se Fable.IdentExpr
está definido como this
se é a palavra-chave this ou não, já que precisa ser traduzida para self
em Python.
https://github.com/fable-compiler/Fable/blob/nagareyama/src/Fable.Transforms/Fable2Babel.fs#L792
Isso é engraçado, o lance estava na verdade no Fable 2 AST, mas eu o removi do Fable 3 no esforço de simplificar o AST, já que ele era usado apenas em um lugar, mas acho que podemos adicioná-lo novamente.
Sobre "isso", é complicado. Preciso verificar novamente porque no F # AST isso é representado de maneira diferente em construtores ou métodos (há alguns comentários sobre isso espalhados pelo código). Há uma propriedade IsThisArgument para identificadores em Fable AST, mas deixe-me verificar se isso é apenas para o primeiro argumento de membros desanexados ou se também funciona para "real this".
@alfonsogarciacaro Eu poderia usar um pouco de ajuda sobre como lidar com o código compilado "interno" vs "usuário", por exemplo, exceção tem, por exemplo, .message
, mas como devo traduzir isso? Posso ter certeza de que o código compilado pelo usuário nunca terá um .message
e sempre terminará como, por exemplo, Test__Message_229D3F39(x, e)
portanto, eles nunca interferirão? Ou eu tenho que envolver uma exceção detectada em, por exemplo, JsException
object e adicionar uma propriedade .message
apenas para ter certeza? Como Fable faz isso de F # para Babel? Você acompanha os tipos ou?
Por exemplo: como ex.Message.StartsWith
em F # se torna ex.message.indexOf
em JS? Precisarei traduzir isso para str(ex).index
em Python (ou envolver os objetos).
Na verdade, temos um problema com as exceções F # (como em: exceção Foo de int * int) que não corrigi antes do lançamento do Fable 3 😅 System.Exception é traduzido como JS Error, então eles têm um campo .message. Mas as exceções F # não são derivadas de JS Error por motivos de desempenho (acho que agora tentar acessar .Message simplesmente falha). Há uma discussão sobre isso em algum lugar. Para o seu caso e para exceção geral, acho que você pode assumir por enquanto o Fable acessa as propriedades .message e .stack.
Como queremos que a seleção do idioma esteja no Fable? Hoje podemos selecionar, por exemplo, TypeScript usando --typescript
. Portanto, poderíamos selecionar Python usando --python
. Mas devemos parar de gerar .js
então? Provavelmente deveríamos, pois o JS gerado pode não ser válido se, por exemplo, Emit for usado. Mas às vezes queremos ver o JS. Devemos ter uma opção para --javascript
ou devemos exigir que o usuário execute sem --python
para obter o JavaScript? @alfonsogarciacaro
@dbrattli --typescript
gera apenas .ts
arquivos, então --python
pode gerar apenas .py
arquivos, se você preferir.
Mas, tecnicamente, eles são mutuamente exclusivos, então devemos introduzir um novo switch --language
, com valores JavaScript/TypeScript/Python
?
@ncave Sim, acho uma boa ideia algo como [-lang|--language {"JavaScript"|"TypeScript"|"Python"}]
onde JavaScript
pode ser o padrão. Já comecei a adicionar uma propriedade de linguagem para o compilador, então posso tentar corrigir as opções de linha de comando também? https://github.com/fable-compiler/Fable/pull/2345/files#diff -9cb94477ca17c7556e6f79d71ed20b71740376f7f3b00ee0ac3fdd7e519ac577R12
Algum status. O suporte a Python está ficando melhor. Muitos recursos de linguagem já estão disponíveis. A grande tarefa restante é portar a biblioteca de fábulas. Para tornar isso mais fácil, a biblioteca de fábulas do Python foi movida para o repositório, para que possamos converter e reutilizar os muitos arquivos da biblioteca de fábulas .fs
para Python. No entanto, muitos deles contêm código emitido por JS que precisa ser interceptado e tratado "manualmente". Uma configuração de teste para Python (baseada em pytest) está em vigor atualmente com 43 testes aprovados, então isso torna muito mais fácil desenvolver e adicionar novos recursos (e mostra o que está funcionando).
Crie uma biblioteca de fábulas para Python: dotnet fsi build.fsx library-py
Execute testes Python: dotnet fsi build.fsx test-py
@dbrattli
Idealmente, devemos tentar converter mais de fable-library
em F #. List
e Seq
já estão lá, Array
usa um pequeno número de métodos nativos, talvez eles possam ser separados.
Em um tópico um pouco diferente, na sua opinião quais são os principais benefícios de implementar um novo codegen de linguagem do Babel AST, em vez de diretamente do Fable AST?
@ncave Python e JavaScript são bastante próximos (imo), então é relativamente fácil reescrever Babel AST. Transferi um pouco de JS para Python anteriormente, por exemplo, RxJS para RxPY. Também temos o benefício de todas as correções (de bugs) feitas "upstream". Dito isso, poderíamos reutilizar a maior parte de Fable2Babel.fs
e transformar diretamente em Python usando um Fable2Python.fs
que combina / recolhe Fable2Babel.fs
e Babel2Python.fs
. Na verdade, pensei sobre isso hoje cedo. Em seguida, torna-se mais como uma bifurcação e as correções de bugs provavelmente precisariam ser aplicadas a ambos os lugares. Por enquanto, acho que não há problema em transformar Babel AST, mas, eventualmente, podemos querer transformar Fable AST diretamente.
Isso é ótimo @dbrattli! Muito obrigado por todo o seu trabalho. Na verdade, o que eu tinha em mente era colocar a maior parte do código do Fable em uma biblioteca e liberar as diferentes implementações como ferramentas dotnet independentes: fable (-js), fable-py (ou qualquer outro nome). Talvez isso nos dê mais liberdade nos ciclos de lançamento, mas estou totalmente ok em ter uma única ferramenta dotnet que pode compilar para ambas as linguagens. Isso pode ser mais simples, principalmente no início.
Sobre a biblioteca de fábulas, sim, se possível em vez de reescrever em python os arquivos .ts atuais, seria bom portá-los para F #. Devemos tentar isolar as partes usando Emit
e adaptá-las ao Python usando #if
:
#if FABLE_COMPILER_PYTHON
[<Emit("print($0)")>]
let log(x: obj) = ()
// Other native methods ...
#else
[<Emit("console.log($0)")>]
let log(x: obj) = ()
// ...
#endif
Ou podemos adicionar um novo atributo Emit "multilíngue":
type EmitLangAttribute(macros: string[]) =
inherit Attribute()
[<EmitLang([|"js:console.log($0)"; "py:print($0)"|])>]
let log(x: obj) = ()
Obrigado @alfonsogarciacaro e @ncave , são ótimas ideias. Vou testá-los para ver o que funciona. Vejo o benefício de traduzir a biblioteca de fábulas para F #, então tentarei ajudar aqui, uma vez que o compilador Python é bom o suficiente para lidar com os arquivos ali. Usar #if
deve ajudar muito, e provavelmente poderíamos evitar #if
s movendo as emissões para arquivos de biblioteca específicos de linguagem (já que tenho um arquivo .fsproj
separado para Python.
@alfonsogarciacaro por que não ter dois atributos de emissão? Emit para JS e EmitPy para python. O transpiler Python deve relatar um waening ao encontrar emit para JS etc.
Por meus 2 centavos, gosto da baixa carga cognitiva atual de ter um único atributo Emit que simplesmente emite. Imo, o que @dbrattli está fazendo agora faz sentido (arquivo de projeto diferente para cada versão de idioma fable-library
). Podemos separar as diferentes emissões de linguagem em seus próprios arquivos, que implementam interfaces (ou módulos) bem definidas a serem chamadas posteriormente a partir da implementação comum do F #.
Um bom exemplo pode ser converter o Array.Helpers em uma interface (ou apenas deixá-lo como um módulo) que pode ser implementado para cada idioma de destino. Talvez este esforço de migrar mais de fable-library
para F # (bem como a opção de compilador --language
) possa ir para um PR separado deste, para que possa ir mais rápido e ser mais facilmente contribuído para .
Essa é uma boa ideia @ncave : +1: Na verdade, já estamos fazendo isso ao isolar o código nativo em projetos Fable multiplataforma (.net e js). Para simplificar as coisas, eu provavelmente apenas adicionaria um novo arquivo Native.fs
ao Fable.Library e teria submódulos lá conforme necessário, como:
namespace Fable.Library.Native # or just Native
module Array = ..
module Map = ..
Devemos mover para lá qualquer coisa que use Emit
ou valores de Fable.Core.JS
.
Trabalhando em assíncrono, estou tentando transferir Async.ts
e AsyncBuilder.ts
para F #, ou seja, tendo Async.fs
e AsyncBuilder.fs
. Mas agora tenho o problema de meu arquivo de teste com o código:
async { return () }
|> Async.StartImmediate
gera código Python (o código JS deve ser muito semelhante):
startImmediate(singleton.Delay(lambda _=None: singleton.Return()))
AsyncBuilder, entretanto, não contém métodos, mas há AsyncBuilder__Delay_458C1ECD
. Então eu obtenho AttributeError: 'AsyncBuilder' objeto não tem atributo 'Delay'.
class AsyncBuilder:
def __init__(self):
namedtuple("object", [])()
def AsyncBuilder__ctor():
return AsyncBuilder()
def AsyncBuilder__Delay_458C1ECD(x, generator):
def lifted_17(ctx_1):
def lifted_16(ctx):
generator(None, ctx)
protectedCont(lifted_16, ctx_1)
return lifted_17
...
Alguma dica de como posso lidar com isso? @ncave @alfonsogarciacaro. Existe uma maneira de fazer classes F # gerar classes com métodos, ou fazer meus testes gerarem código que usa o AsyncBuilder__Delay_458C1ECD
estático em vez de .Delay()
?
@dbrattli Normalmente, a resposta para mutilação é usar interfaces para os métodos que você deseja que não sejam mutiladas.
Em uma nota relacionada, o suporte assíncrono é um dos recursos que talvez sejam mais adequados para uma tradução para o Python nativo asyncio
?
@ncave Ok, é assim que funciona 😄 Obrigado pelo insight. Vou tentar colocar o construtor atrás de uma interface e ver se isso ajuda (mesmo que os construtores F # não sejam interfaces, provavelmente deve funcionar bem). Quanto a async
o plano era usar assíncio Python nativo ( veja aqui ), mas tive problemas com python cannot reuse already awaited coroutine
, então preciso retardá-los atrás de uma função. De qualquer forma, descobri que poderia muito bem usar um construtor task
ou asyncio
para os aguardáveis nativos do Python. A simplicidade de AsyncBuilder.ts
me atraiu a tentar portá-lo para F #.
Usar uma interface para evitar mutilações deve funcionar como diz @ncave . Também tentamos algumas outras abordagens:
NoOverloadSuffix
: evita que o Fable adicione o sufixo de sobrecarga. Obviamente, as sobrecargas não funcionarão neste caso.Então, podemos referenciar os métodos usando Naming.buildNameWithoutSanitation
:
src/fable-library/System.Text.fs
:Nesse caso, você só precisa adicionar a classe ao dicionário replacedModules
:
Portanto, infelizmente, ainda não existe uma abordagem consistente para vincular ao código da biblioteca de fábulas escrito em F # de Replacements
. Talvez usemos esta oportunidade para concordar com um agora.
Obrigado pelos insights muito interessantes @ncave e @alfonsogarciacaro. Com interfaces me deparei com outro problema. Como lidar com unit
em Python? Minha interface é assim:
type IAsyncBuilder =
abstract member Bind<'T, 'U> : IAsync<'T> * ('T -> IAsync<'U>) -> IAsync<'U>
abstract member Combine<'T> : IAsync<unit> * IAsync<'T> -> IAsync<'T>
abstract member Delay<'T> : (unit -> IAsync<'T>) -> IAsync<'T>
abstract member Return<'T> : value: 'T -> IAsync<'T>
...
O problema é que Return
gera:
class AsyncBuilder:
def Return(self, value):
return protectedReturn(value)
....
Isso parece bom e provavelmente é bom para JS, mas em Python você deve fornecer um argumento se a função receber um argumento. Assim, você obterá um erro se chamar x.Return()
quando 'T
for unit
. Minha primeira reação foi fazer uma sobrecarga:
abstract member Return : unit -> IAsync<unit>
mas isso tem seus próprios problemas. Minha solução atual (que parece feia é):
abstract member Return<'T> : [<ParamArray>] value: 'T [] -> IAsync<'T>
...
member this.Return<'T>([<ParamArray>] values: 'T []) : IAsync<'T> =
if Array.isEmpty values then
protectedReturn (unbox null)
else
protectedReturn values.[0]
Mas isso exige que eu tenha um tratamento personalizado para cada função de argumento único genérico. Talvez eu deva, em vez disso, apenas fazer com que cada função de argumento único aceite nulo (Nenhum em Python) como entrada. Por exemplo:
class AsyncBuilder:
def Return(self, value=None):
return protectedReturn(value)
....
Qual seria a maneira certa de lidar com esse problema?
IIRC, para métodos com assinatura unit -> X
as chamadas não têm argumentos (como no F # AST), mas para lambdas ou, neste caso, argumentos genéricos preenchidos com unit
chamadas / aplicativos têm unit
argumento no Fable AST (também no F # AST). Este argumento é removido no auxiliar transformCallArgs
da etapa Fable2Babel. Talvez pudéssemos adicionar uma condição lá e deixar o argumento unit quando a linguagem de destino for python:
Olá @ncave , @alfonsogarciacaro , gostaria de algumas informações sobre como lidar com [<Inject>]
parâmetros em Python. Como isso é feito no Php @thinkbeforecoding? Por exemplo, funções como (em Array.fs
):
let map (f: 'T -> 'U) (source: 'T[]) ([<Inject>] cons: Cons<'U>): 'U[] =
let len = source.Length
let target = allocateArrayFromCons cons len
for i = 0 to (len - 1) do
target.[i] <- f source.[i]
target
Meu código não está gerando o parâmetro cons
e é necessário para Python.
map(fn ar)
É opcional para JS? Como posso detectar o atributo no código e torná-lo ideal? Por exemplo
def map(f, source, cons=None):
...
Php também é explícito em parâmetros opcionais. Tive que adicionar um sinalizador IsOptional no modelo de argumento.
Existem dois usos para o atributo Inject
no Fable. Um é principalmente experimental (para emular de alguma forma typeclasses e também para evitar ter que embutir uma função para resolver informações de tipo genérico) e você não deve se preocupar muito com isso.
O outro uso é interno em métodos de biblioteca de fábulas que precisam de algumas informações extras passadas por um argumento resolvido em tempo de compilação:
No entanto, essas funções não são chamadas diretamente, portanto Fable "não pode ver" o atributo Inject
. Por este motivo, usamos o arquivo ReplacementsInject.fs que diz quais funções na biblioteca requerem injeção. Veja como ele é usado: https://github.com/fable-compiler/Fable/blob/522f6aad211102271538798aeb90f4aed1f77dd6/src/Fable.Transforms/Replacements.fs#L988-L1019
Este arquivo foi criado automaticamente no início com este script que detecta quais funções em Fable.Library têm o último argumento decorado com Inject
. Mas acho que em um ponto paramos de atualizar e IIRC as últimas atualizações para o ReplacementInjects. arquivo foram feitos manualmente.
Sabendo disso, provavelmente posso me livrar das informações IsOptional ... Vou verificar
@alfonsogarciacaro Parece haver um problema com injectArg
para códigos como:
type Id = Id of string
let inline replaceById< ^t when ^t : (member Id : Id)> (newItem : ^t) (ar: ^t[]) =
Array.map (fun (x: ^t) -> if (^t : (member Id : Id) newItem) = (^t : (member Id : Id) x) then newItem else x) ar
let ar = [| {|Id=Id"foo"; Name="Sarah"|}; {|Id=Id"bar"; Name="James"|} |]
replaceById {|Id=Id"ja"; Name="Voll"|} ar |> Seq.head |> fun x -> equal "Sarah" x.Name
replaceById {|Id=Id"foo"; Name="Anna"|} ar |> Seq.head |> fun x -> equal "Anna" x.Name
Aqui, o Array.map
não fará com que o construtor seja injetado. O código irá transpilar para JS sem nenhum argumento injetado:
return map((x) => (equals(newItem.Id, x.Id) ? newItem : x), ar);
Isso é um inseto? Este código funcionará em JS (por sorte?), Mas não para Python.
Ah, desculpe! Esqueci completamente sobre isso, mas temos uma "otimização" em que o construtor de matriz não é injetado para a matriz JS "padrão": https://github.com/fable-compiler/Fable/blob/4ecab5549ab6fcaf317ab9484143420671ded43b/src/Fable.Transforms/ Replacements.fs # L1005 -L1009
Em seguida, apenas padronizamos para global Array
quando nada é passado: https://github.com/fable-compiler/Fable/blob/4ecab5549ab6fcaf317ab9484143420671ded43b/src/fable-library/Array.fs#L25 -L28
Não tenho certeza de porque fiz isso, mas acho que para salvar alguns bytes no pacote para os casos mais comuns. Podemos remover essa otimização para o branch next
se ajudar você.
Ok, obrigado. Vou tentar ver se consigo fazer isso funcionar. Não acho que precisamos desligá-lo, só precisamos gerar um valor vazio (nulo) para o terceiro argumento para que o Python não engasgue. ie:
| Types.arrayCons ->
match genArg with
| Number(numberKind,_) when com.Options.TypedArrays ->
args @ [getTypedArrayName com numberKind |> makeIdentExpr]
| _ -> args @ [ Expr.Value(ValueKind.Null genArg, None) ]
Para JS, ele irá gerar:
map((x_3) => (equals(newItem_1.Id, x_3.Id) ? newItem_1 : x_3), ar, null))).Name);
e para Python:
def lifted_53(x_2):
return newItem_1 if (equals(newItem_1["Id"], x_2["Id"])) else (x_2)
return map(lifted_53, ar, None)
Algo assim seria uma solução aceitável?
Isso deve funcionar. Talvez possamos usar None
vez disso. Em um ponto, estávamos removendo None
args na última posição em Fable2Babel, parece que estamos fazendo isso agora em FSharp2Fable, mas podemos adicionar uma verificação extra aqui: https://github.com/fable-compiler/ Fable / blob / c54668b42b46c0538374b6bb2e283af41a6e5762 / src / Fable.Transforms / Fable2Babel.fs # L1082 -L1095
BTW, desculpe, eu não verifiquei este PR em profundidade 😅 Mas se não estiver quebrando os testes, poderíamos mesclá-lo em breve como POC como fizemos com PHP. Isso é porque estou planejando (o tempo permitir) fazer alguma refatoração para tornar mais fácil direcionar várias linguagens com Fable, e provavelmente será mais fácil se Python já estiver no branch next
para isso. Também evitamos ter que manter muitos branches divergentes em sincronia (main, next, python).
Ok @alfonsogarciacaro , vou ver se consigo gerar None ao invés de null. Podemos fundi-lo em breve. Ainda está quebrando alguns dos testes Python existentes, mas há apenas alguns restantes agora e eu devo consertar isso dentro de uma semana ou assim. Também é necessário criar Fable.Core.PY.fs
com as emissões necessárias. A reescrita de Babel2Python -> Fable2Python foi mais difícil do que eu esperava, mas estou finalmente chegando perto de voltar aos trilhos. Vou limpar um pouco o PR e deixá-lo pronto para ser mesclado.
@alfonsogarciacaro Corrigi todos os testes, exceto um. No entanto, está relacionado ao mesmo problema antigo que tive com Babel 😱 Preciso de uma maneira de saber se Fable.Get
é usado em um tipo AnonymousRecord
uma vez que eles se transformam em Python dict, e preciso usar subscrito ie []
acesso e não .dotted
. O problema é que o lado esquerdo pode ser qualquer coisa, objeto, chamada de função, ... então é difícil saber. Existe uma maneira melhor. Devemos ter outro GetKind
para registros anônimos, ou qual é a melhor maneira de fazer isso?
Eu vejo este no FSharp2Fable. Como posso saber depois que o FieldGet foi gerado por um AnonRecord?
// Getters and Setters
| FSharpExprPatterns.AnonRecordGet(callee, calleeType, fieldIndex) ->
let r = makeRangeFrom fsExpr
let! callee = transformExpr com ctx callee
let fieldName = calleeType.AnonRecordTypeDetails.SortedFieldNames.[fieldIndex]
let typ = makeType ctx.GenericArgs fsExpr.Type
return Fable.Get(callee, Fable.FieldGet(fieldName, false), typ, r)
Como conversamos no Discord @dbrattli , acho que você pode verificar o tipo de chamada para ver se é um registro anônimo. Mas se você precisar de uma abordagem mais sistemática, não deve haver problema em adicionar mais informações em FieldGet
, embora, neste ponto, provavelmente devamos declarar um registro separado para minimizar alterações importantes e evitar confusão com outros campos booleanos (F # deve ter nomes de campo de caso de união mais estritos btw):
type FieldGetInfo =
{ Name: string
IsMutable: bool
IsAnonymousRecord: bool }
type GetKind =
| FieldGet of info: FieldGetInfo
| ...
Obrigado @alfonsogarciacaro. Isso funcionou! 🎉
Próximo problema:
testCase "Map.IsEmpty works" <| fun () ->
let xs = Map.empty<int, int>
xs.IsEmpty |> equal true
let ys = Map [1,1; 2,2]
ys.IsEmpty |> equal false
Para JS, compila para:
Testing_testCase("Map.isEmpty works", () => {
Testing_equal(true, isEmpty_1(ofSeq([], {
Compare: (x_1, y_1) => compare(x_1, y_1),
})));
Testing_equal(false, isEmpty_1(ofSeq([[1, 1]], {
Compare: (x_2, y_2) => comparePrimitives(x_2, y_2),
})));
})
Observe que ofSeq
é chamado com dois argumentos. No entanto, ofSeq
leva apenas um único argumento:
let ofSeq elements =
Map<_, _>.Create elements
export function ofSeq(elements) {
return FSharpMap_Create(elements);
}
Por que aquela expressão de objeto com Compare
adicionada quando ofSeq
é chamada?
Hmm, eu preciso dar uma olhada nisso. Parece um bug, ofSeq
deve aceitar um comparador (funções semelhantes como MapTree.ofSeq
e Set.ofSeq
do). A configuração é um pouco complicada, então talvez em algum ponto as coisas tenham saído de sincronia: temos um arquivo chamado ReplacementsInject.fs que é usado por Replacements para indicar quais métodos precisam de injeção de argumento. Existe um script em algum lugar para gerar este arquivo automaticamente, mas já faz um tempo que não o usamos e não tenho certeza se funciona com a versão mais recente do FCS. Vou verificar, obrigado por apontar isso!
Eu adicionei uma correção temporária. Pronto para revisão 🎉 https://github.com/fable-compiler/Fable/pull/2345
Comentários muito úteis
Este é um ótimo trabalho @dbrattli! 👏 👏 👏 Minha ideia original com o Fable era fornecer uma maneira de compilar facilmente o F # para outras linguagens. Para o bem ou para o mal, o Fable evoluiu com JS como seu foco principal, então seria necessário algum trabalho para torná-lo mais agnóstico quanto ao idioma, mas eu estaria aberto a isso. Alguns comentários às suas perguntas:
Repo igual ou diferente? Eu seria a favor de manter o máximo de código comum possível. Usar repositórios diferentes provavelmente os fará divergir bastante rápido (já é difícil manter em sincronia o branch com o suporte de cotação), mas também precisamos tornar o código modular o suficiente para que não tenhamos condições em todos os lugares, dependendo do idioma de destino. Uma solução intermediária seria tentar extrair o máximo de código possível para um "Núcleo" da Fable (não podemos usar o Core 😉) e então ter diferentes repositórios para as implementações da linguagem.
Babel AST: Isso foi usado porque originalmente estávamos contando com o Babel para imprimir o código. Em um mundo ideal, agora que temos nossa impressora, deve ser possível removê-la. Mas há muito trabalho acontecendo na etapa Fable-to-Babel, então seria difícil. Pelo mesmo motivo, seria preferível reutilizar o máximo de código possível a partir desta etapa, talvez tornando o Babel AST algo mais genérico para linguagens não funcionais. Alguns exemplos:
Tornar o F # mais pitônico? Com o Fable, sempre tentei encontrar um equilíbrio em que a semântica do .NET fosse sacrificada às vezes para evitar sobrecarga. Por exemplo, usamos apenas o número JS em vez de ter um tipo específico para ints (embora façamos isso para longs), isso se aplicaria ao Python
int
, presumo. No entanto, muitas contribuições foram feitas para respeitar a semântica do .NET em pontos específicos para evitar resultados inesperados, por exemplo, em divisões de inteiros ou conversões numéricas explícitas.De qualquer forma, quando você diz "mais pythônico", você quer dizer estilo de código ou APIs nativas? O Fable não tenta suportar a maior parte do .NET BCL, mas no final precisamos suportar a função / operadores comuns porque é a isso que os desenvolvedores estão acostumados (mesmo os novatos, porque isso eles encontrariam em tutoriais). É bom dar a opção de usar as APIs nativas em qualquer caso ( Fable.Extras é uma boa jogada nesse sentido). Por exemplo,
JS.console.log
geralmente formata objetos mais bem do queprintfn
, mas precisamos oferecer suporte a este último porque é o que os desenvolvedores mais usam.