Lo scopo di ecr all'interno di crystal: o come posso passare variabili e oggetti nei modelli ECR?

Come principiante nel linguaggio Crystal, ho ancora difficoltà a capire alcuni dei concetti in esso contenuti e a sviluppare una sensazione di codifica in Crystal.

Quando mi imbatto in problemi difficili, che risolvo o comincio a capire, ne parlo sul blog, in modo che gli altri ne possano beneficiare - poiché la mancanza di documentazione è molto sentita (da me) a volte.

Avanti!

Ecco la documentazione per ECR (nella v.0.27, l'attuale ultima versione di Crystal):

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

Potete dare un'occhiata al codice sorgente di ECR, i modelli di cristallo incorporati, qui:

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

ECR è un linguaggio di template a tempo di compilazione. Non si può usare ECR per elaborare i template a tempo di esecuzione!

Macro per dominare il mondo

Quando usi i modelli ECR in Crystal (anche se li usi per mezzo di Kemal), stai usando delle macro.

Crystal ha un linguaggio di macro che permette di astrarre e riutilizzare il codice; come primo passo della compilazione, le macro vengono valutate e il codice risultante viene "incollato" dove è stata la chiamata alla macro.

Poi Crystal passa a compilare il codice.

Come funziona per l'ECR?

Prendiamo, per esempio, il def_to_s(nome del file) nel codice dell'esempio:

richiedere "ecr"

classe Saluto
  def initialize(@name : String)
  end

  ECR.def_to_s "greeting.ecr"
fine

# saluto.ecr
Saluto, !

Greeting.new("John").to_s #=> Saluto, John!

Crystal chiamerà la macro da_a_s a tempo di compilazione con "greeting.ecr" passato in esso.
La macro è definita qui:
https://github.com/crystal-lang/crystal/blob/c9d1eef8fde5c7a03a029d64c8483ed7b4f2fe86/src/ecr/macros.cr

macro def_to_s(filename)

  def to_s(__io__)

     ECR.embed {{filename}}, "__io__"

  fine

fine

la vostra classe sarà riscritta così nel primo passo:

richiedere "ecr"

classe Saluto
  def initialize(@name : String)
  end

  def to_s(__io__)
    ECR.embed "greeting.ecr", "__io__"
fine

Vedete cosa è appena successo? Un metodo to_s è stato aggiunto alla vostra classe, che a sua volta contiene una macro. Vediamo cosa fa questa macro:

macro embed(nome file, io_name)

    \eseguire("ecr/processo", {{nome del file}}, {{io_name.id.stringify}}) }}

fine

Questa è la chiamata centrale di ECR. Quello che fa è compilare (? / eseguire) un'applicazione diversa ecr/processo e gli passa filename e io_name come parametri.

Il ritorno è il risultato dell'output di quell'applicazione.

Cosa significa il backslash?

"È possibile definire un che genera una o più definizioni di macro. Dovete sfuggire alle espressioni macro della macro interna facendole precedere da un carattere backslash \ per evitare che vengano valutate dalla macro esterna".

È essenzialmente una macro annidata!

ecr/processo è definito qui:

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

è essenzialmente un wrapper intorno a ECR.process_file (ricordate, questa non è più una macro - questa è un'applicazione il cui output sarà alla fine incollato nel vostro codice Crystal!)

ecr/processore è definito qui:

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

Elabora il file e crea una stringa che restituisce.

Ecco un piccolo estratto:

str << nome_del_buffer

str << " << "

string.inspect(str)

str << '\n'

buffer_name è quello che abbiamo passato a Crystal sopra (__io__ - identificato dal suo id in io_name.id.stringify).

Anche i valori di uscita e di controllo vengono elaborati. Inoltre, l'output di debug viene incollato per voi (usando # come commento):

append_loc(str, filename, line_number, column_number)

Fondamentalmente, il codice che metti nei file ECR viene incollato direttamente nel tuo codice - nel posto che hai specificato. È tutto fatto da macro e dall'esecuzione di uno speciale parser ECR.

Il codice ECR non è a conoscenza delle vostre variabili - se ottenete fallimenti di scope, fallimenti di variabili non definite, ecc, non è dovuto al fatto che non "l'avete passato in ECR", ma al fatto di non essere nello scope giusto.

Prova quello che stavi cercando di fare con il codice ECR direttamente nel tuo codice principale.

Dimostrazione

Ecco una dimostrazione di come "usare" una classe all'interno del vostro codice ECR in Kemal. La classe annida un ulteriore snippet ECR da rendere con il contesto della classe.

il file debug.cr:

richiedere "kemal"

require "./../macros.cr"

modulo Debug
   includere le macro

      classe DClass
           @test : Stringa
           def initialize()
               @test = "Stringa di prova"
           fine
           ECR.def_to_s "src/views/snippets/debug_snippet.ecr"
       fine

  ottenere "/debug" do |env|

    loggedin = false

    mrender "debug"
   fine

fine

il file debug.ecr:

<% content_for “main” do%>

Informazioni di debug

.

Incorporamento del frammento decorato

<%= DClass.new() %>

.

il codice rilevante per la macro mrender, definita in macros.cr:

macro mrender(nome del file)
   rendere "src/views/#{{{{filename}}}.ecr", "src/views/layout.ecr"
fine   

Usa il macro rendering di Kemal che ti permette di specificare un layout per la tua vista. (Non avete bisogno di questo nel vostro codice necessariamente, è solo dato per completezza qui)

il file src/views/snippets/debug_snippet.ecr:

Frammento di debug

L'uscita:

immagine

Mettere tutto insieme:

  1. L'utente chiama /debug nel suo webbrowser
  2. La macro get di Kemal corrisponde al percorso "/debug". È definita nel mio debug.cr
    1. La variabile locale loggato sarà impostato su false
    2. debug.ecr sarà processato dalle macro ECR, e incollato in "debug.cr" (la rappresentazione AST), come se fosse stato inserito direttamente da voi
      1. la variabile locale loggato sarà valutato come falso (in fase di esecuzione)
      2. chiamiamo DClass.new()e chiedergli la sua rappresentazione stringa - che è definita dal metodo def to_s.
        1. possiamo chiamare DClass.new(), perché è definito nello stesso modulo in cui stiamo eseguendo. Di nuovo, pensate semplicemente che il vostro codice ECR sia incollato proprio lì, sotto la definizione della classe.
        2. gli chiediamo la sua rappresentazione in forma di stringa perché usiamo la sintassi .

Ok, diamo un'occhiata a cosa succede nella chiamata DClass.new:

      classe DClass
          @test : Stringa
           def initialize()
               @test = "Stringa di prova"
           fine
           ECR.def_to_s "src/views/snippets/debug_snippet.ecr"
       fine

all'inizializzazione, viene impostata una stringa @test. Questo è un istanza dell'istanza di DClass che abbiamo appena creato. (Potete vedere che è una variabile di istanza perché ha una "@" davanti. Due "@@" sarebbero una variabile di classe)

Questa variabile di istanza è usata / visualizzata in debug_snippet.ecr

Abbiamo già discusso come funziona ECR.def_to_s.

In effetti, dopo essere passata attraverso lo stadio di macro, questa classe avrebbe un aspetto simile a questo:

      classe DClass
          @test : Stringa
           def initialize()
               @test = "Stringa di prova"
           fine
           def to_s(__io__)

                __io__ << "Debug snippet\n"

                __io__ << ""

                __io__ << @test

                __io__ << ""

           fine
       fine

Usando questa tecnica si possono definire classi per rendere frammenti di codice ECR, invece di impostare e passare ogni nome di variabile manualmente.

Spero che questo articolo ti aiuti, e che tu lo trovi invece di crescere frustrato - vorrei aver avuto qualcosa come questo per guidarmi nell'iniziare Sorriso

Riferimenti

Riferimento Kemal:

Fate riferimento a questa pagina per saperne di più sulle macro:

NB: Nodi AST: nodi dell'albero della sintassi astratta

Questo mi ha indicato la direzione giusta, complimenti!