매크로

매크로는 컴파일 시간에 AST 노드를 받아 코드를 생성하는 메서드입니다.

macro define_method(name, content)
  def {{name}}
    {{content}}
  end
end

# 다음을 생성
#
#     def foo
#       1
#     end
define_method foo, 1

foo #=> 1

매크로의 정의부는 일반적인 크리스탈 코드와 유사합니다. 차이점이라면 AST 노드를 조작하는 문법이 추가되어 있다는 것입니다. 생성된 코드는 반드시 유효한 크리스탈 코드여야 합니다. 즉 end 없는 def나, when 표현식 없는 case 등은 생성할 수 없습니다.

범위

최상위에 선언된 매크로는 어디서든 사용할 수 있습니다. 최상위 매크로가 private으로 표시된다면 그 파일에서만 사용할 수 있습니다.

클래스나 모듈에 매크로를 선언할 수도 있으며, 그 경우 해당 모듈이나 클래스에서만 사용할 수 있습니다. 매크로는 또한 상위 클래스와 포함한 모듈 등과 같은 상속 연쇄를 따릅니다.

예를 들어, with ... yield를 호출하여 리시버 객체를 받은 블락은 해당 객체의 상속 연쇄에 정의되어 있는 매크로에 접근할 수 있습니다.

class Foo
  macro emphasize(value)
    "***#{ {{value}} }***"
  end

  def yield_with_self
    with self yield
  end
end

Foo.new.yield_with_self { emphasize(10) } #=> "***10***"

클래스와 모듈에 정의된 매크로 또한 바깥에서 호출할 수 있습니다.

class Foo
  macro emphasize(value)
    "***#{ {{value}} }***"
  end
end

Foo.emphasize(10) # => "***10***"

보간

위에서 보았듯 {{...}}를 이용하여 AST 노드를 붙여넣거나 보간합니다.

해당 노드는 그대로 붙여넣어진다는 사실에 주의해야 합니다. 앞서 본 예시에서 심벌을 넘긴다면 생성된 코드는 유효하지 않게 됩니다.

# 다음을 생성
#
#     def :foo
#       1
#     end
define_method :foo, 1

:foo가 매크로에 넘겨졌기 때문에 그대로 보간의 결과가 됩니다. 이 경우처럼 식별자가 필요하다면 ASTNode#id 메서드를 사용하면 됩니다.

매크로 호출

컴파일 시간에는 AST 노드에 미리 정해진 종류의 메서드만을 호출할 수 있습니다. 가상의 Crystal::Macros 모듈에 정리되어 있습니다.

예를 들어, 위의 코드에서 ASTNode#id를 호출하여 문제를 해결할 수 있습니다.

macro define_method(name, content)
  def {{name.id}}
    {{content}}
  end
end

# 올바르게 다음을 생성
#
#     def foo
#       1
#     end
define_method :foo, 1

조건문

{% if condition %} ... {% end %} 구문을 통해 조건적으로 코드를 생성할 수 있습니다.

macro define_method(name, content)
  def {{name}}
    {% if content == 1 %}
      "one"
    {% else %}
      {{content}}
    {% end %}
  end
end

define_method foo, 1
define_method bar, 2

foo #=> one
bar #=> 2

일반 코드와 비슷하게, Nop, NilLiteral과 거짓인 BoolLiteral거짓인 값으로 취급되며, 나머지는 모두 참인 값으로 취급됩니다.

매크로 조건문은 매크로 정의가 아닐 때에도 사용할 수 있습니다.

{% if env("TEST") %}
  puts "지금은 테스트 모드"
{% end %}

반복문

다음과 같이 ArrayLiteral의 요소를 반복할 수 있습니다.

macro define_dummy_methods(names)
  {% for name, index in names %}
    def {{name.id}}
      {{index}}
    end
  {% end %}
end

define_dummy_methods [foo, bar, baz]

foo #=> 0
bar #=> 1
baz #=> 2

위의 경우 index 변수를 쓰지 않을 수도 있습니다.

HashLiteral의 요소를 반복하는 것은 다음과 같습니다.

macro define_dummy_methods(hash)
  {% for key, value in hash %}
    def {{key.id}}
      {{value}}
    end
  {% end %}
end
define_dummy_methods({foo: 10, bar: 20})
foo #=> 10
bar #=> 20

매크로 반복문은 매크로 정의가 아닐 때에도 사용할 수 있습니다.

{% for name, index in ["foo", "bar", "baz"] %}
  def {{name.id}}
    {{index}}
  end
{% end %}

foo #=> 0
bar #=> 1
baz #=> 2

가변 인자와 쪼개기

매크로는 가변 인자를 받을 수 있습니다.

macro define_dummy_methods(*names)
  {% for name, index in names %}
    def {{name.id}}
      {{index}}
    end
  {% end %}
end

define_dummy_methods foo, bar, baz

foo #=> 0
bar #=> 1
baz #=> 2

인자는 ArrayLiteral에 담겨 매크로에 전달됩니다.

추가로 ArrayLiteral을 보간할 때 *를 사용한다면, 쉼표로 쪼개어 인자를 합치게 될 것입니다.

macro println(*values)
   print {{*values}}, '\n'
end

println 1, 2, 3 # outputs 123\n

타입 정보

매크로를 호출할 때 현재 객체의 타입을 특수한 인스턴스 변스 @type을 통해 접근할 수 있습니다. 이 변수의 타입은 TypeNode이며, 컴파일 시간에 타입 정보를 읽을 수 있습니다.

매크로가 클래스 메서드에서 호출될 때라고 해도 @type은 항상 인스턴스 타입입니다.

상수

매크로에서 상수를 읽을 수 있습니다.

VALUES = [1, 2, 3]

{% for value in VALUES %}
  puts {{value}}
{% end %}

상수가 타입 표기일 경우, 그 값은 TypeNode입니다.

results matching ""

    No results matching ""