O escopo do ecr dentro do cristal: ou como eu passo em variáveis e Objetos em modelos ECR?

Como principiante na linguagem Crystal, ainda tenho dificuldade em entender alguns dos conceitos nela contidos, e desenvolver uma sensação de codificação em Crystal.

Quando me deparo com problemas difíceis, que eu resolvo ou começo a entender, faço um blog sobre eles, para que outros possam se beneficiar - pois a falta de documentação é sentida severamente (por mim) às vezes.

Avante!

Aqui está a documentação do ECR (no v.0.27, a última versão atual do Crystal):

https://crystal-lang.org/api/0.27.0/ECR.html

Você pode dar uma olhada no código fonte do ECR, os modelos de cristal embutidos, aqui:

https://github.com/crystal-lang/crystal/tree/c9d1eef8fde5c7a03a029d64c8483ed7b4f2fe86/src/ecr

A ECR é uma linguagem de modelo em tempo de compilação. Não é possível usar o ECR para processar modelos em tempo de execução!

Macros para governar o mundo

Quando você usa os modelos ECR em Crystal ( também se você os usar por meio do Kemal ), você está usando macros.

O Crystal tem uma linguagem de macro que permite abstrair e reutilizar código; Como primeiro passo da compilação, as macros são avaliadas e o código resultante é "colado" no local onde a chamada de macro foi feita.

Depois a Crystal continua a compilar o código.

Como é que isto funciona para a ECR?

Vamos tomar, por exemplo, o def_to_s(nome_de_arquivo) macro no código do exemplo:

exigir "ecr"

saudação de classe
  def initialize(@name : String)
  final

  ECR.def_to_s "greeting.ecr"
final

# saudação.ecr
Saudação, !

Greeting.new("John").to_s #=> Saudação, John!

A Crystal vai chamar a macro def_to_s em tempo de compilação, com "greeting.ecr" sendo passado para ele.
A macro é definida aqui:
https://github.com/crystal-lang/crystal/blob/c9d1eef8fde5c7a03a029d64c8483ed7b4f2fe86/src/ecr/macros.cr

macro def_to_s(nome_de_arquivo)

  def to_s(__io__)

     ECR.embed {{{{nome do ficheiro}}, "__io__".

  final

final

a sua turma será reescrita desta forma no primeiro passo:

exigir "ecr"

saudação de classe
  def initialize(@nome : String)
  final

  def to_s(__io__)
    ECR.embed "saudação.ecr", "__io__".
final

Viu o que acabou de acontecer? um método to_s foi adicionado à sua classe, que por si só contém uma macro. Vamos ver o que esta macro faz:

macro embed(nome_do_arquivo, io_nome)

    \{{{{rrorocesso("ecr/processo", {{{nome do ficheiro}}, {{io_nome.id.stringify}}}}}

final

Esta é a chamada principal da ECR. O que ela faz, é compilar (? / executa) uma aplicação diferente ecr/processo e passa-lhe o nome de ficheiro e io_nome como parâmetros.

O retorno é o resultado da saída dessa aplicação.

O que significa a contrabarra?

"É possível definir um macro que gera uma ou mais definições de macro. Você deve escapar de expressões macro da macro interna precedendo-as com uma barra invertida para evitar que elas sejam avaliadas pela macro externa".

É essencialmente uma macro aninhada!

ecr/processo é definido aqui:

https://github.com/crystal-lang/crystal/blob/c9d1eef8fde5c7a03a029d64c8483ed7b4f2fe86/src/ecr/process.cr

é essencialmente um wrapper em torno do ECR.process_file (lembre-se, isto não é mais uma macro - esta é uma aplicação cuja saída será eventualmente colada no seu código Crystal!)

O ecr/processador é definido aqui:

https://github.com/crystal-lang/crystal/blob/c9d1eef8fde5c7a03a029d64c8483ed7b4f2fe86/src/ecr/processor.cr

Ele processa o arquivo, e cria uma cadeia que devolve.

Aqui está um pequeno excerto:

str << nome_tampão

str <<< " << "

string.inspect(str)

str << '\n'

buffer_name é o que passamos ao Crystal acima (__io__ - identificado pelo seu id em io_name.id.stringify).

Os valores de saída e controle também são processados. Além disso, a saída de depuração é colada para você (usando # como um comentário):

append_loc(str, filename, line_number, column_number)

Basicamente, o código que você coloca nos arquivos ECR é colado diretamente no seu código - no local que você especificou. Tudo isso é feito por macros e executando um analisador ECR especial.

O código ECR não tem conhecimento das suas variáveis - se você obtiver falhas de escopo, falhas de variáveis indefinidas, etc., não é devido a você não "ter passado para o ECR", mas devido a não estar no escopo correto.

Tente o que você estava tentando fazer com o código ECR diretamente no seu código principal.

Demonstração

Aqui está uma demonstração de como "usar" uma classe dentro do seu código ECR em Kemal. A classe aninha um trecho adicional de ECR para ser renderizado com o contexto da classe.

o arquivo debug.cr:

precisam de "kemal".

requerem "./.../macros.cr".

módulo Debug
   incluir Macros

      classe classe DClass
           @teste : Corda
           def initialize()
               @test = "Test String" (Teste de Cordas)
           final
           ECR.def_to_s "src/views/snippets/debug_snippet.ecr"
       final

  get "/debug" do |env|

    logado = falso

    depuração" do rrender
   final

final

o arquivo de debug.ecr:

<% content_for “main” do%>

Debug information

<%= loggedin %>

Incorporating decorado snippet

<%= DClass.new() %>

<% end %>

o código relevante para a macro-renderização, definido em macros.cr:

macro mrender(nome do arquivo)
   renderizar "src/views/#{{{{{filename}}}.ecr", "src/views/layout.ecr"
final   

Ele usa a macro renderização do Kemal que permite especificar um layout para sua visualização. (Você não precisa disso em seu código necessariamente, ele é dado apenas para a completude aqui)

o arquivo src/views/snippets/debug_snippet.ecr:

Desbug snippet

A saída:

imagem

A juntar tudo:

  1. O usuário chama / depura em seu navegador da web
  2. O macro get de Kemal vai coincidir com a rota "/debug". Está definido no meu debug.cr
    1. A variável local logado em será falso
    2. debug.ecr será processado pelas macros ECR, e colado em "debug.cr". (a representação AST), como se tivesse sido directamente introduzida por si
      1. a variável local logado em será avaliado como falso (em tempo de execução)
      2. nós chamamos DClass.new()e pedir-lhe a sua representação em cadeia - que é definida pelo método def to_s.
        1. podemos chamar DClass.new(), porque está definido no mesmo módulo em que estamos executando. Novamente, simplesmente pense como o seu código ECR sendo colado ali mesmo, abaixo da definição da classe.
        2. pedimos a sua representação em cadeia porque usamos a sintaxe

OK, vamos ver o que acontece no DClass.new call:

      classe classe DClass
          @teste : Corda
           def initialize()
               @test = "Test String" (Teste de Cordas)
           final
           ECR.def_to_s "src/views/snippets/debug_snippet.ecr"
       final

na inicialização, uma string @test é definida. Este é um exemplo variável da instância DClass que acabamos de criar. (Você pode ver que é uma variável de instância porque ela tem um "@" na frente dela. Dois "@@" seria uma variável de classe)

Esta variável de instância é usada / exibida em debug_snippet.ecr

Já discutimos como o ECR.def_to_s funciona antes.

Efectivamente, depois de passar pela fase macro, esta turma ficaria algo parecido com isto:

      classe classe DClass
          @teste : Corda
           def initialize()
               @test = "Test String" (Teste de Cordas)
           final
           def to_s(__io__)

                __io__ << "Debug snippet\n"

                __io__ << ""

                __io__ << @test

                __io__ << ""

           final
       final

Usando esta técnica é possível definir classes para renderizar trechos de código ECR, em vez de configurar e passar manualmente cada nome de variável.

Espero que este artigo o ajude, e que você o encontre em vez de ficar frustrado - gostaria de ter algo assim para me orientar no início Sorria

Referências

Referência Kemal:

Consulte esta página para obter mais informações sobre macros:

NB: Nós AST: nós de árvore de sintaxe abstrata

Isto apontou-me na direcção certa, kudos!!