English · Português

15 — Macros e Decorators

Meta do capítulo: anexar comportamento reutilizável a funções com decorators, e usar as macros de tempo de compilação de Aura. Exemplos: ../../examples/macros.aura, ../../examples/compile_time_macros.aura.

Decorators

Um decorator é @name ou @name(args) acima de um def (ou de uma classe). Decorators em um campo são rejeitados (E320). Modificadores de membro e decorators podem aparecer em qualquer ordem.

@staticmethod
public def max(a, b) -> int { return a }

class Rect {
  public @property
  def area() -> int { return 4 }

  public @classmethod
  def create(cls) { return cls() }
}

Os decorators de membro de classe são @property, @staticmethod e @classmethod (capítulo 07). Um decorator em uma função simples pode ser qualquer chamável, incluindo um que você escreve:

def trace(f) {
  def wrapper(x) {
    print(f"calling with {x}")
    return f(x)
  }
  return wrapper
}

@trace
def square(x) -> int { return x * x }

def main() {
  print(square(4))
}

Macros de runtime embutidas

Aura traz um pequeno prelúdio de decorators. O prelúdio é injetado automaticamente apenas quando uma macro é usada.

@debug
def multiply(a, b) -> int {
  return a * b
}

@timeit
def sum_to(n) -> int {
  let mut total = 0
  for i in range(n) { total += i }
  return total
}

@memoize
def fib(n) -> int {
  if n < 2 { return n }
  return fib(n - 1) + fib(n - 2)
}

@cache(maxsize=256)
def expensive(n) -> int { return n * n }

def main() {
  print(multiply(6, 7))     // DEBUG: enter/exit lines, then 42
  print(sum_to(100))        // a timing line, then 4950
  print(fib(20))            // 6765
  print(expensive(12))      // 144
}
Decorator Efeito
@debug imprime os argumentos e o resultado da chamada
@timeit imprime quanto tempo a chamada levou
@memoize cache de resultados ilimitado
@cache(maxsize=n) cache LRU limitado

Estes são decorators de runtime: eles envolvem a função Python emitida.

Macros de tempo de compilação

Uma segunda camada expande antes de qualquer Python ser emitido. A macro recebe seus operandos como AST citado e retorna AST de substituição, então nada sobrevive ao runtime a menos que a expansão escolha emiti-lo. Importe-as de macros:

import macros

def main() {
  assert_eq(2 + 2, 4)        // evaluate both once, then assert
  assert_ne("a", "b")
  static_assert(true)        // checked while compiling

  let mut a = 1
  let mut b = 2
  swap(a, b)                 // hygienic temporary; a,b change
  print(a, b)                // 2 1

  print(identity(41) + 1)    // identity disappears; prints 42
  print(stringify(42))       // folds the literal at compile time
  print(debug_value(a))      // prints "a = 2", yields the value
}
Macro Expansão
assert_eq(a, b) avalia ambos uma vez, afirma igualdade
assert_ne(a, b) avalia ambos uma vez, afirma desigualdade
static_assert(x) checada em tempo de compilação; um não-literal é erro
identity(x) expande para x
stringify(x) dobra um literal para uma string em tempo de compilação
swap(a, b) troca dois lvalues via um temporário higiênico
debug_value(x) imprime x = <value> uma vez, depois produz o valor

Uma macro que introduz bindings usa nomes higiênicos, então ela nunca pode capturar um local no local da chamada.

Runtime vs tempo de compilação

  Decorators de runtime Macros de tempo de compilação
Quando rodam quando a função é chamada durante a transpilação
Forma @decorator em um def expressões tipo chamada no corpo
Exemplos @debug, @timeit, @memoize, @cache assert_eq, swap, stringify
Use quando você quer um wrapper em torno de uma chamada você quer o código reescrito antes do emit

Ordem dos decorators

Quando vários decorators são empilhados eles se aplicam de baixo para cima: o mais próximo do def envolve primeiro, o mais externo roda primeiro. A ordem muda o comportamento. Usando um wrapper trace local:

def trace(f) {
  def wrapper(x) {
    print(f"calling with {x}")
    return f(x)
  }
  return wrapper
}

@memoize
@trace
def fib(n) -> int {
  if n < 2 { return n }
  return fib(n - 1) + fib(n - 2)
}

def main() {
  print(fib(5))
}
calling with 5
calling with 4
calling with 3
calling with 2
calling with 1
calling with 0
5

Com @memoize mais externo, o cache é checado antes do trace, então cada valor de n é rastreado exatamente uma vez. Troque as duas linhas e o trace roda em toda chamada, porque a recursão alcança a função rastreada antes do cache.

O que você aprendeu

  • Decorators se anexam a def/classe (nunca a um campo); @property, @staticmethod, @classmethod e decorators personalizados.
  • Decorators de runtime embutidos @debug, @timeit, @memoize, @cache.
  • Macros de tempo de compilação de macros, incluindo assert_eq, swap, stringify, static_assert.

Próximo passo

CLI e Tooling →