Percursos
/
Factor
Factor
/
Exercícios
/
Moldes de Sinais
Moldes de Sinais

Moldes de Sinais

Exercício de aprendizagem

Introdução

Já sabes construir uma quotation a partir de peças e executá-la. Uma quotation é apenas uma sequência de código, por isso as palavras de sequência que já conheces também se aplicam a ela:

USING: kernel math prettyprint quotations sequences ;

{ [ 1 ] [ 2 + ] [ 3 * ] } concat >quotation call .   ! => 9
[ 1 + ] dup append .                                 ! => [ 1 + 1 + ]

O append junta duas quotations, o concat achata um array delas, o map transforma dados em quotations e o >quotation torna o resultado passível de ser chamado. O MACRO: pega nessa mesma competência e transporta-a para o tempo de compilação.

O que é uma macro

Uma definição MACRO: parece uma palavra normal, mas o seu corpo é executado enquanto o teu programa está a ser compilado, e não enquanto é executado. O corpo tem de deixar uma quotation na pilha. O compilador insere depois essa quotation no local da chamada: substitui a chamada da macro pelo código que o corpo produziu. Em tempo de execução já não resta nenhuma macro, apenas o código gerado.

USING: kernel macros sequences ;

MACRO: twice ( quot -- spliced )
    dup append ;

Onde quer que escrevas [ 1 + ] twice, o compilador:

  1. executa o corpo com [ 1 + ] na pilha,
  2. dup append constrói a quotation [ 1 + 1 + ],
  3. insere o seu código, 1 + 1 +, nesse local.

Assim, uma palavra que chama twice compila como se tivesses escrito 1 + 1 + à mão:

: inc-twice ( x -- y ) [ 1 + ] twice ;

5 inc-twice .   ! => 7

O contraste essencial: uma palavra normal que recebe uma quotation chama-a com call em tempo de execução. Uma macro nunca chama nada: entrega ao compilador código novo para compilar no seu lugar.

As entradas têm de ser literais

Como o corpo é executado em tempo de compilação, as entradas de uma macro têm de ser conhecidas nessa altura. Têm de ser literais no local da chamada: uma quotation [ … ], um número, um array, uma string. Um valor que só existe em tempo de execução não pode ser entregue a uma macro.

! DOCTEST: SKIP — illustrative call sites, not standalone code
[ 1 + ] twice          ! ok: the quotation is a literal
[ 1 + ] 3 repeat-quot  ! ok: the quotation and the number are literals

O conjunto de ferramentas

O corpo de uma macro é código normal que devolve uma quotation. As palavras que já conheceste fazem a maior parte do trabalho:

para construir… usa
[ A B ] a partir de [ A ] e [ B ] append
uma quotation a partir de um array delas concat >quotation
uma quotation por cada elemento de uma sequência map, depois concat >quotation

Mais duas palavras, de sequences e kernel, completam o conjunto.

<repetition> (em sequences) cria uma sequência de n cópias de um elemento. O seu efeito na pilha é ( length elt -- repetition ): a contagem vem primeiro. O resultado é uma sequência virtual, por isso achata-a da forma habitual:

3 [ 1 + ] <repetition> concat >quotation .   ! => [ 1 + 1 + 1 + ]

curry (em kernel) acrescenta um valor ao início de uma quotation, produzindo uma nova quotation que empilha esse valor primeiro:

3 [ + ] curry .          ! => [ 3 + ]
{ 1 2 3 } [ . ] curry .  ! => [ { 1 2 3 } . ]

O curry é a forma de incorporar um elemento concreto numa quotation: é exatamente o que transforma { 10 20 30 } e [ + ] em [ 10 + ] [ 20 + ] [ 30 + ].

Porque é que isto é mais simples do que parece

Nalgumas linguagens, as macros precisam de sintaxe especial de quote/unquote e de maquinaria extra para evitar colisões de nomes. O Factor não precisa de nada disso: uma quotation já é uma sequência de palavras resolvidas; o parser procurou cada dup, + e append quando leu o [ … ]. Assim, o corpo de uma macro é apenas a manipulação de sequências que já fazes, que por acaso devolve uma quotation.

Um primo próximo: $[ … ]

Uma macro devolve uma quotation para ser inserida numa chamada. Quando, em vez disso, queres um valor simples calculado em tempo de análise, $[ code ] executa code enquanto o ficheiro está a ser lido e incorpora o resultado no código-fonte como literal:

! DOCTEST: SKIP — compile-time value, shown for contrast
USING: calendar literals ;

CONSTANT: build-time $[ now ]

O $[ … ] vem do vocabulário literals, que reúne as palavras para interpolar resultados de tempo de análise em literais. É mencionado por uma questão de completude. Os quatro stencils que vais escrever, twice, repeat-quot, compose-many e each-literal, são todos declarações MACRO:.

Instruções

Estás a fazer a manutenção do conjunto de ferramentas de geração de código de uma estação de sinais. Cada macro é um stencil: um pequeno molde que o compilador usa para gerar código especializado em cada ponto de chamada.

Tu escreves apenas as quatro macros. O ficheiro de testes define pequenas palavras que servem de invólucro às tuas macros e que as chamam (uma macro só pode ser observada através do código que gera); depois verifica os resultados de execução desses invólucros. Deixa os invólucros como estão.

Em cada tarefa, o corpo recebe na pilha os valores literais do ponto de chamada e tem de deixar uma quotation. As linhas "compila para" mostram o código linear que a tua quotation deve produzir.

1. Duas vezes

twice recebe uma quotation literal e devolve uma quotation que executa a original duas vezes seguidas.

! : inc-twice ( x -- y ) [ 1 + ] twice ;
! body receives [ 1 + ]  ->  must build [ 1 + 1 + ]
5 inc-twice .   ! => 7

2. N repetições

repeat-quot recebe uma quotation literal e uma contagem literal n e devolve uma quotation que executa a original n vezes seguidas. n = 0 devolve a quotation vazia [ ].

! : inc-thrice ( x -- y ) [ 1 + ] 3 repeat-quot ;
! body receives [ 1 + ] 3  ->  must build [ 1 + 1 + 1 + ]
5 inc-thrice .   ! => 8

3. Compor várias

compose-many recebe um array literal de quotations e devolve uma única quotation que executa cada uma por sua vez. Um array vazio devolve a quotation vazia.

! : pipeline ( x -- y ) { [ 1 + ] [ 2 * ] [ 1 - ] } compose-many ;
! body receives { [ 1 + ] [ 2 * ] [ 1 - ] }  ->  must build [ 1 + 2 * 1 - ]
5 pipeline .   ! => 11

4. Iteração desenrolada

each-literal recebe uma sequência literal e uma quotation. Devolve uma quotation que, para cada elemento da sequência, empilha esse elemento e depois executa a quotation. É uma iteração que acontece em tempo de compilação, por isso o código gerado é linear.

! : sum-three ( -- n ) 0 { 10 20 30 } [ + ] each-literal ;
! body receives { 10 20 30 } [ + ]  ->  must build [ 10 + 20 + 30 + ]
sum-three .   ! => 60

Esta é a mais estrutural das quatro: tanto o comprimento da sequência como os valores dos seus elementos moldam o código gerado. As dicas acompanham a sua construção passo a passo.

Editar via GitHub A ligação abre numa nova janela ou separador
Factor Exercism

Estás pronto para começar Moldes de Sinais?

Inscreve-te no Exercism para aprenderes e dominares Factor com 47 conceitos163 exercícios, e mentoria humana real, tudo grátis.