Computer >> 컴퓨터 >  >> 프로그램 작성 >> Ruby

Ruby 템플릿:인터프리터 굽기

오늘 우리는 끈적끈적한 스트룹(스트룹 와플의 두 반쪽을 서로 붙게 하는 시럽)으로 물건을 붙이기 때문에 커피 위에 스트룹 와플을 따뜻하게 해주길 바랍니다. 시리즈의 처음 두 부분에서 우리는 Lexer와 Parser를 구웠고 이제 Interpreter를 추가하고 그 위에 스트룹을 부어서 붙입니다.

재료

괜찮은! 주방을 구울 준비를 하고 재료를 식탁에 올려 보겠습니다. 인터프리터는 작업을 수행하기 위해 이전에 생성된 추상 구문 트리(AST)와 템플릿에 포함하려는 데이터의 두 가지 구성 요소 또는 정보가 필요합니다. 이 데이터를 environment이라고 부를 것입니다. .

AST를 순회하기 위해 방문자 패턴을 사용하여 인터프리터를 구현합니다. 방문자(따라서 우리의 인터프리터)는 노드를 매개변수로 받아들이고 이 노드를 처리하고 잠재적으로 visit을 호출하는 일반 방문 방법을 구현합니다. 현재 노드에 대한 의미에 따라 노드의 자식 중 일부(또는 전체)와 함께 메서드를 다시 사용합니다.

module Magicbars
  class Interpreter
    attr_reader :root, :environment
 
    def self.render(root, environment = {})
      new(root, environment).render
    end
 
    def initialize(root, environment = {})
      @root = root
      @environment = environment
    end
 
    def render
      visit(root)
    end
 
    def visit(node)
      # Process node
    end
  end
end

계속하기 전에 작은 Magicbars.render도 만들어 보겠습니다. 템플릿과 환경을 받아들이고 렌더링된 템플릿을 출력하는 메소드입니다.

module Magicbars
  def self.render(template, environment = {})
    tokens = Lexer.tokenize(template)
    ast = Parser.parse(tokens)
    Interpreter.render(ast, environment)
  end
end

이를 통해 AST를 직접 구성하지 않고도 인터프리터를 테스트할 수 있습니다.

Magicbars.render('Welcome to {{name}}', name: 'Ruby Magic')
# => nil

놀랍게도 현재 아무 것도 반환하지 않습니다. 이제 visit 구현을 시작해 보겠습니다. 방법. 빠른 알림으로 이 템플릿의 AST는 다음과 같습니다.

이 템플릿의 경우 네 가지 노드 유형을 처리해야 합니다. Template , Content , ExpressionIdentifier . 이렇게 하려면 거대한 case visit 내부의 문 방법. 그러나 이것은 꽤 빨리 읽을 수 없게 됩니다. 대신 Ruby의 메타프로그래밍 기능을 사용하여 코드를 좀 더 체계적이고 읽기 쉽게 유지해 보겠습니다.

module Magicbars
  class Interpreter
    # ...
 
    def visit(node)
      short_name = node.class.to_s.split('::').last
      send("visit_#{short_name}", node)
    end
  end
end

이 메서드는 노드를 수락하고, 클래스 이름을 가져오고, 모든 모듈을 제거합니다(이 작업을 수행하는 다른 방법에 관심이 있는 경우 문자열 정리에 대한 기사를 확인하세요). 그 다음에는 send를 사용합니다. 이 특정 유형의 노드를 처리하는 메서드를 호출합니다. 각 유형의 메소드 이름은 복조된 클래스 이름과 visit_로 구성됩니다. 접두사. 메서드 이름에 대문자를 사용하는 것은 다소 이례적인 일이지만 메서드의 의도를 꽤 명확하게 보여줍니다.

module Magicbars
  class Interpreter
    # ...
 
    def visit_Template(node)
      # Process template nodes
    end
 
    def visit_Content(node)
      # Process content nodes
    end
 
    def visit_Expression(node)
      # Process expression nodes
    end
 
    def visit_Identifier(node)
      # Process identifier nodes
    end
  end
end

visit_Template 구현부터 시작하겠습니다. 방법. 모든 statements을 처리해야 합니다. 노드의 및 결합 결과.

def visit_Template(node)
  node.statements.map { |statement| visit(statement) }.join
end

다음으로 visit_Content를 살펴보겠습니다. 방법. 콘텐츠 노드가 문자열을 래핑하기 때문에 방법은 매우 간단합니다.

def visit_Content(node)
  node.content
end

이제 visit_Expression으로 넘어가 보겠습니다. 자리 표시자를 실제 값으로 대체하는 방법입니다.

def visit_Expression(node)
  key = visit(node.identifier)
  environment.fetch(key, '')
end

마지막으로 visit_Expression의 경우 환경에서 가져올 키를 확인하는 방법을 사용하려면 visit_Identifier를 구현해 보겠습니다. 방법.

def visit_Identifier(node)
  node.value
end

이 네 가지 방법을 사용하면 템플릿을 다시 렌더링하려고 할 때 원하는 결과를 얻을 수 있습니다.

Magicbars.render('Welcome to {{name}}', name: 'Ruby Magic')
# => Welcome to Ruby Magic

블록 표현식 해석

우리는 간단한 gsub 할 수 있습니다. 이제 더 복잡한 예를 살펴보겠습니다.

Welcome to {{name}}!
 
{{#if subscribed}}
  Thank you for subscribing to our mailing list.
{{else}}
  Please sign up for our mailing list to be notified about new articles!
{{/if}}
 
Your friends at {{company_name}}

참고로 해당 AST는 다음과 같습니다.

아직 처리하지 않는 노드 유형은 하나뿐입니다. visit_BlockExpression입니다. 마디. 어떻게 보면 visit_Expression과 비슷합니다. 노드이지만 값에 따라 statements을 계속 처리합니다. 또는 inverse_statements BlockExpression의 노드.

def visit_BlockExpression(node)
  key = visit(node.identifier)
 
  if environment[key]
    node.statements.map { |statement| visit(statement) }.join
  else
    node.inverse_statements.map { |statement| visit(statement) }.join
  end
end

메서드를 살펴보면 두 분기가 매우 유사하고 visit_Template과도 유사하다는 것을 알 수 있습니다. 방법. 모두 Array의 모든 노드 방문을 처리합니다. , 그래서 visit_Array를 추출해 봅시다. 정리하는 방법입니다.

def visit_Array(nodes)
  nodes.map { |node| visit(node) }
end

새 방법을 사용하면 visit_Template에서 일부 코드를 제거할 수 있습니다. 및 visit_BlockExpression 방법.

def visit_Template(node)
  visit(node.statements).join
end
 
def visit_BlockExpression(node)
  key = visit(node.identifier)
 
  if environment[key]
    visit(node.statements).join
  else
    visit(node.inverse_statements).join
  end
end

이제 인터프리터가 모든 노드 유형을 처리하므로 복잡한 템플릿을 렌더링해 보겠습니다.

Magicbars.render(template, { name: 'Ruby Magic', subscribed: true, company_name: 'AppSignal' })
# => Welcome to Ruby Magic!
#
#
#  Please sign up for our mailing list to be notified about new articles!
#
#
# Your friends at AppSignal

거의 제대로 보인다. 그러나 자세히 살펴보면 subscribed: true를 제공했음에도 불구하고 메일링 리스트에 가입하라는 메시지가 표시됩니다. 환경에서. 옳지 않은 것 같습니다...

도우미 메서드에 대한 지원 추가

템플릿을 다시 살펴보면 if 블록 표현식에서 subscribed의 값을 찾는 대신 환경에서 visit_BlockExpression if의 값을 찾고 있습니다. . 환경에 존재하지 않으므로 호출은 nil을 반환합니다. , 이는 거짓입니다.

여기서 멈추고 핸들바가 아닌 Mustache를 모방하고 if를 제거한다고 선언할 수 있습니다. 템플릿에서 원하는 결과를 얻을 수 있습니다.

Welcome to {{name}}!
 
{{#subscribed}}
  Thank you for subscribing to our mailing list.
{{else}}
  Please sign up for our mailing list to be notified about new articles!
{{/subscribed}}
 
Your friends at {{company_name}}

그러나 우리가 재미있을 때 왜 멈추는가? 더 나아가 도우미 메서드를 구현해 보겠습니다. 다른 용도로도 유용할 수 있습니다.

간단한 표현식에 도우미 메서드 지원을 추가하여 시작하겠습니다. reverse을 추가하겠습니다. 전달된 문자열을 뒤집는 도우미. 또한 debug 주어진 값의 클래스 이름을 알려주는 메서드입니다.

def helpers
  @helpers ||= {
    reverse: ->(value) { value.to_s.reverse },
    debug: ->(value) { value.class }
  }
end

간단한 람다를 사용하여 이러한 도우미를 구현하고 이름으로 조회할 수 있도록 해시에 저장합니다.

다음으로 visit_Expression을 수정해 보겠습니다. 환경에서 값 조회를 시도하기 전에 도우미 조회를 수행합니다.

def visit_Expression(node)
  key = visit(node.identifier)
 
  if helper = helpers[key]
    arguments = visit(node.arguments).map { |k| environment[k] }
 
    return helper.call(*arguments)
  end
 
  environment[key]
end

지정된 식별자와 일치하는 도우미가 있는 경우 메서드는 모든 인수를 방문하여 값을 조회하려고 시도합니다. 그런 다음 메서드를 호출하고 모든 값을 인수로 전달합니다.

Magicbars.render('Welcome to {{reverse name}}', name: 'Ruby Magic')
# => Welcome to cigaM ybuR
 
Magicbars.render('Welcome to {{debug name}}', name: 'Ruby Magic')
# => Welcome to String

이제 마지막으로 if를 구현해 보겠습니다. 및 unless 돕는 사람. 인수 외에도 노드의 statements 해석을 계속할지 여부를 결정할 수 있도록 두 개의 람다를 전달합니다. 또는 inverse_statements .

def helpers
  @helpers ||= {
    if: ->(value, block:, inverse_block:) { value ? block.call : inverse_block.call },
    unless: ->(value, block:, inverse_block:) { value ? inverse_block.call : block.call },
    # ...
  }
end
 

visit_BlockExpression에 필요한 변경 사항 visit_Expression에서 수행한 작업과 유사합니다. , 이번에만 두 개의 람다도 전달합니다.

def visit_BlockExpression(node)
  key = visit(node.identifier)
 
  if helper = helpers[key]
    arguments = visit(node.arguments).map { |k| environment[k] }
 
    return helper.call(
      *arguments,
      block: -> { visit(node.statements).join },
      inverse_block: -> { visit(node.inverse_statements).join }
    )
  end
 
  if environment[key]
    visit(node.statements).join
  else
    visit(node.inverse_statements).join
  end
end

그리고 이것으로 베이킹이 완료되었습니다! 렉서, 파서 및 인터프리터의 세계로 이 여정을 시작한 복잡한 템플릿을 렌더링할 수 있습니다.

Magicbars.render(template, { name: 'Ruby Magic', subscribed: true, company_name: 'AppSignal' })
# => Welcome to Ruby Magic!
#
#
#  Thank you for subscribing to our mailing list.
#
#
# Your friends at AppSignal

표면만 긁기

이 3부작 시리즈에서 우리는 템플릿 언어를 만드는 기본 사항을 다뤘습니다. 이러한 개념은 해석된 프로그래밍 언어(예:Ruby)를 만드는 데 사용할 수도 있습니다. 확실히, 우리는 몇 가지 사항(적절한 오류 처리 🙀)을 간과하고 오늘날 프로그래밍 언어의 토대를 표면적으로만 긁었습니다.

시리즈를 즐기셨기를 바라며 더 많은 내용을 원하시면 Ruby Magic 목록을 구독하십시오. 지금 스트룹 와플이 너무 먹고 싶으시다면 저희에게 전화를 주시면 저희가 여러분에게 연료를 공급할 수도 있습니다!