Fable: Suporte Python para Fable

Criado em 4 jan. 2021  ·  54Comentários  ·  Fonte: fable-compiler/Fable

Descrição

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:

  1. Adicionar suporte decente para Python
  2. Torne o Fable mais agnóstico para o JS e ofereça suporte a vários idiomas de destino.

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.

Casos de uso:

  • F # em execução no notebook Jupyter com kernel Python, ou seja, semelhante ao que é feito com Hy e Calysto Hy .
  • Script F # usando Python para reduzir o atrito
  • Suporte mais fácil a ambientes integrados como micro: bit e Rasberry PI .
  • Compartilhando modelo de dados entre o código F # e Python

Coisas para discutir e decidir:

  • O Python para F # deve ser mais Pythônico do que .NET? Já existe suporte .NET para Jupyter, que não devemos tentar substituir ou competir com ele. O Fable para Python talvez deva ter como alvo o Python em vez do .NET (se possível) e ser um ajuste mais natural para desenvolvedores Python que buscam o F #.
  • Devemos tentar ser compatíveis com Babel AST ou devemos divergir em nosso próprio Python AST. Isso talvez seja inevitável, mas Babel nos dá um bom ponto de partida.
  • Devemos usar o Fable (como no Peeble ) ou devemos permanecer no Fable e fazer com que o Fable ofereça suporte a vários idiomas. Ter nosso próprio repo dá liberdade e velocidade (por não precisar nos preocupar com JS), mas correremos o risco de sermos deixados para trás e obsoletos com o tempo. Meu sentimento é que devemos nos tornar parte do Fable.
  • Os tipos básicos do Python também são diferentes do F #. Por exemplo, 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 #?

Instalando o POC

O código-fonte do POC reside atualmente aqui:

  1. Instale o Python 3.9 mais recente em https://www.python.org ou brew install [email protected] no Mac. Observe que python pode estar disponível em seu sistema como python , python3 ou ambos.
  2. Clone o repo e mude para o branch python .
  3. Construir biblioteca de fábulas para Python: dotnet fsi build.fsx library-py
  4. Para executar testes: dotnet fsi build.fsx test-py
  5. No Fable, edite 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 #:

Screenshot 2021-01-15 at 13 18 09

Você pode executar o código Python gerado no terminal:

$ python3 quicktest.py

Links

Alguns links relevantes:

Sinta-se à vontade para contribuir com discussões, comentários, ideias e código 😍

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:

    • Mova de AST baseado em expressão para AST de expressão / instrução. Para JS, isso inclui mover declarações de variáveis ​​para o topo da função quando necessário para evitar muitos IIFEs. Acho que será algo semelhante com Python.
    • Transformando a correspondência de padrões (DecisionTree) em instruções if / switch.
    • Otimização da chamada final. Isso é feito usando loops rotulados JS. Em Python, precisaríamos usar outra técnica para ter certeza de não sair de um loop aninhado.
    • Converter importações em identificadores únicos (isso pode ser feito diretamente no Fable AST, se necessário).
  • 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.

Todos 54 comentários

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:

    • Mova de AST baseado em expressão para AST de expressão / instrução. Para JS, isso inclui mover declarações de variáveis ​​para o topo da função quando necessário para evitar muitos IIFEs. Acho que será algo semelhante com Python.
    • Transformando a correspondência de padrões (DecisionTree) em instruções if / switch.
    • Otimização da chamada final. Isso é feito usando loops rotulados JS. Em Python, precisaríamos usar outra técnica para ter certeza de não sair de um loop aninhado.
    • Converter importações em identificadores únicos (isso pode ser feito diretamente no Fable AST, se necessário).
  • 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?

https://github.com/thinkbeforecoding/peeble

@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:

  • O atributo NoOverloadSuffix : evita que o Fable adicione o sufixo de sobrecarga. Obviamente, as sobrecargas não funcionarão neste caso.

https://github.com/fable-compiler/Fable/blob/caa715f1156be29c8dd9b866a03031a1852b3186/src/fable-library/Map.fs#L524 -L527

Então, podemos referenciar os métodos usando Naming.buildNameWithoutSanitation :

https://github.com/fable-compiler/Fable/blob/caa715f1156be29c8dd9b866a03031a1852b3186/src/Fable.Transforms/Replacements.fs#L1956 -L1963

  • Mas depois disso, @ncave escreveu um método para construir nomes mutilados mais ou menos como Fable espera (não me lembro se existem algumas limitações) para que você possa escrever código F # normalmente. Veja por exemplo src/fable-library/System.Text.fs :

https://github.com/fable-compiler/Fable/blob/caa715f1156be29c8dd9b866a03031a1852b3186/src/Fable.Transforms/Replacements.fs#L1314 -L1326

Nesse caso, você só precisa adicionar a classe ao dicionário replacedModules :

https://github.com/fable-compiler/Fable/blob/caa715f1156be29c8dd9b866a03031a1852b3186/src/Fable.Transforms/Replacements.fs#L3075


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:

https://github.com/fable-compiler/Fable/blob/caa715f1156be29c8dd9b866a03031a1852b3186/src/Fable.Transforms/Fable2Babel.fs#L1083 -L1086

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:

  • As funções que constroem um array precisam saber se é um array JS dinâmico ou digitado.
  • Defina e mapeie construtores e outros auxiliares que precisam receber um comparador para saber como um tipo deve ser comparado (semelhante para funções que fazem alguns cálculos de média).

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

Esta página foi útil?
0 / 5 - 0 avaliações