오늘 우리는 끈적끈적한 스트룹(스트룹 와플의 두 반쪽을 서로 붙게 하는 시럽)으로 물건을 붙이기 때문에 커피 위에 스트룹 와플을 따뜻하게 해주길 바랍니다. 시리즈의 처음 두 부분에서 우리는 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
, Expression
및 Identifier
. 이렇게 하려면 거대한 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 목록을 구독하십시오. 지금 스트룹 와플이 너무 먹고 싶으시다면 저희에게 전화를 주시면 저희가 여러분에게 연료를 공급할 수도 있습니다!