Computer >> कंप्यूटर ट्यूटोरियल >  >> प्रोग्रामिंग >> Ruby

Ruby टेम्पलेटिंग में महारत: कुशल टेम्पलेट प्रसंस्करण के लिए पार्सर बनाना

आज, हम Ruby टेम्पलेटिंग में अपनी यात्रा जारी रखते हैं। लेक्सर के साथ, आइए अगले चरण पर चलते हैं: पार्सर।

पिछली बार, हमने स्ट्रिंग इंटरपोलेशन को देखा और बाद में, अपनी खुद की टेम्पलेटिंग भाषा बनाने में गोता लगाया। हमने एक लेक्सर को लागू करके शुरू किया जो एक टेम्पलेट को पढ़ता है और इसे टोकन की एक स्ट्रीम में परिवर्तित करता है। आज, हम साथ में पार्सर को लागू करेंगे। हम भाषा सिद्धांत में भी अपने पैर की उंगलियों को डुबोएंगे।

चलते हैं!

Abstract Syntax Trees

आइए हमारे सरल उदाहरण टेम्पलेट Welcome to {{name}} पर वापस देखें। लेक्सर का उपयोग करके स्ट्रिंग को टोकनाइज़ करने के बाद, हमें टोकन की एक सूची मिलती है जैसे यह।

Magicbars::Lexer.tokenize("Welcome to {{name}}")
# => [[:CONTENT, "Welcome to "], [:OPEN_EXPRESSION], [:IDENTIFIER, "name"], [:CLOSE]]

अंततः, हम टेम्पलेट का मूल्यांकन करना चाहते हैं और एक्सप्रेशन को वास्तविक मानों से बदलना चाहते हैं। चीजों को थोड़ा अधिक चुनौतीपूर्ण बनाने के लिए, हम जटिल ब्लॉक एक्सप्रेशन का मूल्यांकन भी करना चाहते हैं, जिससे पुनरावृत्ति और सशर्त की अनुमति मिलती है।

यह करने के लिए, हमें एक abstract syntax tree (AST) उत्पन्न करना होगा जो टेम्पलेट की तार्किक संरचना का वर्णन करता है। ट्री में नोड्स होते हैं जो अन्य नोड्स को संदर्भित कर सकते हैं या टोकन से अतिरिक्त डेटा स्टोर कर सकते हैं।

हमारे सरल उदाहरण के लिए, वांछित abstract syntax tree इस तरह दिखता है:

एक व्याकरण को परिभाषित करना

व्याकरण को परिभाषित करने के लिए, आइए भाषा के सैद्धांतिक आधार के साथ शुरू करें। अन्य प्रोग्रामिंग भाषाओं की तरह, हमारी टेम्पलेटिंग भाषा एक context-free भाषा है और इसलिए इसे context-free grammar द्वारा वर्णित किया जा सकता है। (Wikipedia के विस्तृत विवरण में गणितीय संकेतन आपको डराने न दें। अवधारणा बहुत सीधी है, और व्याकरण को संकेत करने के अधिक डेवलपर-अनुकूल तरीके हैं।)

एक context-free grammar नियमों का एक समूह है जो वर्णन करता है कि एक भाषा के सभी संभावित तार कैसे निर्मित होते हैं। आइए EBNF संकेतन में हमारी टेम्पलेटिंग भाषा के व्याकरण को देखें:

template = statements;
statements = { statement };
statement = CONTENT | expression | block_expression;
expression = OPEN_EXPRESSION, IDENTIFIER, arguments, CLOSE;
block_expression = OPEN_BLOCK, IDENTIFIER, arguments, CLOSE, statements, [ OPEN_INVERSE, CLOSE, statements ], OPEN_END_BLOCK, IDENTIFIER, CLOSE;
arguments = { IDENTIFIER };

प्रत्येक असाइनमेंट एक नियम को परिभाषित करता है। नियम का नाम बाईं ओर है और दाईं ओर हमारे लेक्सर से अन्य नियम (निचले मामले) या टोकन (ऊपरी मामले) का एक समूह है। नियमों और टोकन को कॉमा , का उपयोग करके जोड़ा जा सकता है या पाइप | प्रतीक का उपयोग करके वैकल्पिक किया जा सकता है। कर्ली ब्रेसेस { ... } के अंदर के नियम और टोकन कई बार दोहराए जा सकते हैं। जब वे ब्रैकेट [ ... ] के अंदर होते हैं, तो उन्हें वैकल्पिक माना जाता है।

उपरोक्त व्याकरण यह वर्णन करने का एक संक्षिप्त तरीका है कि एक टेम्पलेट स्टेटमेंट्स से बना होता है। एक स्टेटमेंट या तो एक CONTENT टोकन, एक एक्सप्रेशन, या एक ब्लॉक एक्सप्रेशन होता है। एक एक्सप्रेशन एक OPEN_EXPRESSION टोकन है, जिसके बाद एक IDENTIFIER टोकन, आर्ग्युमेंट्स, और एक CLOSE टोकन होता है। और एक ब्लॉक एक्सप्रेशन इसका सही उदाहरण है कि प्राकृतिक भाषा का उपयोग करके इसका वर्णन करने की कोशिश करने के बजाय ऊपर दिए गए संकेतन का उपयोग करना बेहतर क्यों है।

ऐसे उपकरण हैं जो व्याकरण परिभाषाओं जैसे ऊपर दिए गए से स्वचालित रूप से पार्सर उत्पन्न करते हैं। लेकिन सच्ची Ruby Magic परंपरा में, आइए कुछ मज़ा लें और पार्सर को स्वयं बनाएं, उम्मीद है कि इस प्रक्रिया में एक या दो चीजें सीखेंगे।

पार्सर बनाना

भाषा सिद्धांत को एक तरफ रखकर, आइए वास्तव में पार्सर बनाने में कूदें। आइए एक और भी न्यूनतम, लेकिन फिर भी वैध, टेम्पलेट के साथ शुरू करें: Welcome to Ruby Magic। इस टेम्पलेट में कोई एक्सप्रेशन नहीं है और टोकन की सूची में सिर्फ एक तत्व होता है। यहां यह कैसा दिखता है:

[[:CONTENT, "Welcome to Ruby Magic"]]

सबसे पहले, हम अपनी पार्सर क्लास सेट करते हैं। यह इस तरह दिखती है:

module Magicbars
  class Parser
    def self.parse(tokens)
      new(tokens).parse
    end
    
    attr_reader :tokens
    
    def initialize(tokens)
      @tokens = tokens
    end
    
    def parse
      # Parsing starts here
    end
  end
end

क्लास टोकन की एक सरणी लेती है और इसे स्टोर करती है। इसमें केवल एक सार्वजनिक विधि parse होती है जो टोकन को AST में परिवर्तित करती है।

हमारे व्याकरण पर वापस देखते हुए, शीर्ष नियम template है। इसका तात्पर्य है कि पार्सिंग प्रक्रिया की शुरुआत में parse एक Template नोड लौटाएगा।

नोड्स सरल क्लासेस हैं जिनका अपना कोई व्यवहार नहीं होता। वे केवल अन्य नोड्स को जोड़ते हैं या टोकन से कुछ मान स्टोर करते हैं। यहां Template नोड कैसा दिखता है:

module Magicbars
  module Nodes
    class Template
      attr_reader :statements
      
      def initialize(statements)
        @statements = statements
      end
    end
  end
end

हमारे उदाहरण को काम करने के लिए, हमें एक Content नोड की भी आवश्यकता है। यह केवल टोकन से टेक्स्ट कंटेंट ("Welcome to Ruby Magic") को स्टोर करता है।

module Magicbars
  module Nodes
    class Content
      attr_reader :content
      
      def initialize(content)
        @content = content
      end
    end
  end
end

अब, आइए पार्स विधि को लागू करें ताकि Template का एक उदाहरण और Content का एक उदाहरण बनाया जा सके और उन्हें सही ढंग से जोड़ा जा सके।

def parse
  Magicbars::Nodes::Template.new(parse_content)
end

def parse_content
  return unless tokens[0][0] == :CONTENT
  Magicbars::Nodes::Content.new(tokens[0][1])
end

जब हम पार्सर चलाते हैं, तो हमें सही परिणाम मिलता है:

Magicbars::Parser.parse(tokens)
# => #>

स्वीकार करते हैं, यह केवल हमारे सरल उदाहरण के लिए काम करता है जिसमें केवल एक कंटेंट नोड है। आइए एक अधिक जटिल उदाहरण पर स्विच करें जिसमें वास्तव में एक एक्सप्रेशन शामिल है: Welcome to {{name}}।

Magicbars::Lexer.tokenize("Welcome to {{name}}")
# => [[:CONTENT, "Welcome to "], [:OPEN_EXPRESSION], [:IDENTIFIER, "name"], [:CLOSE]]

इसके लिए, हमें एक Expression नोड और एक Identifier नोड की आवश्यकता है। Expression नोड पहचानकर्ता के साथ-साथ किसी भी आर्ग्युमेंट को स्टोर करता है (जो, व्याकरण के अनुसार, शून्य या अधिक Identifier नोड्स की एक सरणी है)। अन्य नोड्स की तरह, यहां देखने के लिए ज्यादा कुछ नहीं है।

module Magicbars
  module Nodes
    class Expression
      attr_reader :identifier, :arguments
      
      def initialize(identifier, arguments)
        @identifier = identifier
        @arguments = arguments
      end
    end
  end
end

module Magicbars
  module Nodes
    class Identifier
      attr_reader :value
      
      def initialize(value)
        @value = value.to_sym
      end
    end
  end
end

नए नोड्स के साथ, आइए parse विधि को संशोधित करें ताकि यह नियमित कंटेंट के साथ-साथ एक्सप्रेशन को भी संभाल सके। हम इसे एक parse_statements विधि पेश करके करते हैं जो तब तक parse_statement को कॉल करती रहती है जब तक यह एक मान नहीं लौटाती।

def parse
  Magicbars::Nodes::Template.new(parse_statements)
end

def parse_statements
  results = []
  while result = parse_statement
    results << result
  end
  results
end

parse_statement स्वयं पहले parse_content को कॉल करता है और यदि वह कोई मान नहीं लौटाता है, तो यह parse_expression को कॉल करता है।

def parse_statement
  parse_content || parse_expression
end

क्या आपने देखा कि parse_statement विधि व्याकरण में statement नियम के समान दिखने लगी है? यह वह जगह है जहां व्याक्रम को पहले से स्पष्ट रूप से लिखने का समय लेना बहुत मदद करता है ताकि यह सुनिश्चित किया जा सके कि हम सही रास्ते पर हैं।

अब, आइए parse_content विधि को संशोधित करें ताकि यह केवल पहले टोकन को न देखे। हम इसे इनिशियलाइज़र में एक अतिरिक्त @position इंस्टेंस वेरिएबल पेश करके और वर्तमान टोकन को लाने के लिए इसका उपयोग करके करते हैं।

attr_reader :tokens, :position

def initialize(tokens)
  @tokens = tokens
  @position = 0
end

# ...

def parse_content
  return unless token = tokens[position]
  return unless token[0] == :CONTENT
  @position += 1
  Magicbars::Nodes::Content.new(token[1])
end

parse_content विधि अब वर्तमान टोकन को देखती है और उसके प्रकार की जांच करती है। यदि यह एक CONTENT टोकन है, तो यह स्थिति को बढ़ाता है (क्योंकि वर्तमान टोकन सफलतापूर्वक पार्स हो गया था) और Content नोड बनाने के लिए टोकन के कंटेंट का उपयोग करता है। यदि कोई वर्तमान टोकन नहीं है (क्योंकि हम टोकन के अंत में हैं) या प्रकार मेल नहीं खाता, तो विधि जल्दी बाहर निकलती है और nil लौटाती है।

सुधरे हुए parse_content विधि के साथ, आइए नई parse_expression विधि से निपटें।

def parse_expression
  return unless token = tokens[position]
  return unless token[0] == :OPEN_EXPRESSION
  @position += 1
  identifier = parse_identifier
  arguments = parse_arguments
  if !tokens[position] || tokens[position][0] != :CLOSE
    raise "Unexpected token #{tokens[position][0]}. Expected :CLOSE."
  end
  @position += 1
  Magicbars::Nodes::Expression.new(identifier, arguments)
end

सबसे पहले, हम जांचते हैं कि एक वर्तमान टोकन है और उसका प्रकार OPEN_EXPRESSION है। यदि ऐसा है, तो हम अगले टोकन पर आगे बढ़ते हैं और parse_identifier और parse_arguments को कॉल करके क्रमशः पहचानकर्ता और आर्ग्युमेंट्स को पार्स करते हैं। दोनों विधियां संबंधित नोड्स लौटाएंगी और वर्तमान टोकन को आगे बढ़ाएंगी। जब यह हो जाता है, तो हम सुनिश्चित करते हैं कि वर्तमान टोकन मौजूद है और एक :CLOSE टोकन है। यदि यह नहीं है, तो हम एक त्रुटि उठाते हैं। अन्यथा, हम एक अंतिम बार स्थिति को आगे बढ़ाते हैं, नवनिर्मित Expression नोड को लौटाने से पहले।

इस बिंदु पर, हम कुछ पैटर्न उभरते देखते हैं। हम कई बार अगले टोकन पर आगे बढ़ रहे हैं और हम यह भी जांच रहे हैं कि एक वर्तमान टोकन है और उसका प्रकार। चूंकि उसके लिए कोड थोड़ा बोझिल है, आइए दो हेल्पर विधियों को पेश करें।

def expect(*expected_tokens)
  upcoming = tokens[position, expected_tokens.size]
  if upcoming.map(&:first) == expected_tokens
    advance(expected_tokens.size)
    upcoming
  end
end

def advance(offset = 1)
  @position += offset
end

expect विधि टोकन प्रकारों की एक चर संख्या लेती है और उन्हें टोकन स्ट्रीम में अगले टोकन के विरुद्ध जांचती है। यदि वे सभी मेल खाते हैं, तो यह मिलान करने वाले टोकन के पार आगे बढ़ती है और उन्हें लौटाती है। advance विधि केवल दिए गए ऑफसेट द्वारा @position इंस्टेंस वेरिएबल को बढ़ाती है।

ऐसे मामलों के लिए जहां अगले अपेक्षित टोकन के संबंध में कोई लचीलापन नहीं है, हम एक विधि भी पेश करते हैं जो टोकन मेल न खाने पर एक अच्छा त्रुटि संदेश उठाती है।

def need(*required_tokens)
  upcoming = tokens[position, required_tokens.size]
  expect(*required_tokens) or raise "Unexpected tokens. Expected #{required_tokens.inspect} but got #{upcoming.inspect}"
end

इन हेल्पर विधियों का उपयोग करके, parse_content और parse_expression अब क्लीनर और अधिक पठनीय हैं।

def parse_content
  if content = expect(:CONTENT)
    Magicbars::Nodes::Content.new(content[0][1])
  end
end

def parse_expression
  return unless expect(:OPEN_EXPRESSION)
  identifier = parse_identifier
  arguments = parse_arguments
  need(:CLOSE)
  Magicbars::Nodes::Expression.new(identifier, arguments)
end

अंत में, आइए parse_identifier और parse_arguments को भी देखें। हेल्पर विधियों के लिए धन्यवाद, parse_identifier विधि parse_content विधि जितनी सरल है। एकमात्र अंतर यह है कि यह एक अन्य नोड प्रकार लौटाता है।

def parse_identifier
  if identifier = expect(:IDENTIFIER)
    Magicbars::Nodes::Identifier.new(identifier[0][1])
  end
end

parse_arguments विधि को लागू करते समय, हमने देखा कि यह parse_statements विधि के लगभग समान है। एकमात्र अंतर यह है कि यह parse_statement के बजाय parse_identifier को कॉल करता है। हम एक और हेल्पर विधि पेश करके डुप्लिकेट तर्क से छुटकारा पा सकते हैं।

def repeat(method)
  results = []
  while result = send(method)
    results << result
  end
  results
end

repeat विधि दिए गए विधि नाम को कॉल करने के लिए send का उपयोग करती है जब तक कि यह एक नोड नहीं लौटाता। एक बार ऐसा होने पर, एकत्रित परिणाम (या सिर्फ एक खाली सरणी) लौटाए जाते हैं। इस हेल्पर के साथ, दोनों parse_statements और parse_arguments एक-लाइन विधियां बन जाती हैं।

def parse_statements
  repeat(:parse_statement)
end

def parse_arguments
  repeat(:parse_identifier)
end

इन सभी परिवर्तनों के साथ, आइए टोकन स्ट्रीम को पार्स करने का प्रयास करें:

Magicbars::Parser.parse(tokens)
# => #,
# # >

इसे पढ़ना थोड़ा कठिन है लेकिन यह वास्तव में सही abstract syntax tree है। Template नोड में एक Content और एक Expression स्टेटमेंट है। Content नोड का मान "Welcome to " है और Expression नोड का पहचानकर्ता Identifier नोड है जिसका मान :name है।

ब्लॉक एक्सप्रेशन पार्स करना

हमारे पार्सर कार्यान्वयन को पूरा करने के लिए, हमें अभी भी ब्लॉक एक्सप्रेशन पार्स करने को लागू करना होगा। एक अनुस्मारक के रूप में, यहां वह टेम्पलेट है जिसे हम पार्स करना चाहते हैं:

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}}

ऐसा करने के लिए, आइए पहले एक BlockExpression नोड पेश करें। हालांकि यह नोड थोड़ा अधिक डेटा स्टोर करता है, यह कुछ और नहीं करता और इसलिए बहुत रोमांचक नहीं है।

module Magicbars
  module Nodes
    class BlockExpression
      attr_reader :identifier, :arguments, :statements, :inverse_statements
      
      def initialize(identifier, arguments, statements, inverse_statements)
        @identifier = identifier
        @arguments = arguments
        @statements = statements
        @inverse_statements = inverse_statements
      end
    end
  end
end

Expression नोड की तरह, यह पहचानकर्ता के साथ-साथ किसी भी आर्ग्युमेंट को स्टोर करता है। इसके अतिरिक्त, यह ब्लॉक के स्टेटमेंट्स और इनवर्स ब्लॉक के स्टेटमेंट्स को भी स्टोर करता है।

व्याकरण पर वापस देखते हुए, हम देखते हैं कि ब्लॉक एक्सप्रेशन को पार्स करने के लिए, हमें parse_block_expression के कॉल के साथ parse_statements विधि में संशोधन करना होगा। यह अब व्याकरण में नियम की तरह दिखता है।

def parse_statement
  parse_content || parse_expression || parse_block_expression
end

parse_block_expression विधि स्वयं थोड़ी अधिक जटिल है। लेकिन हमारी हेल्पर विधियों के लिए धन्यवाद, यह अभी भी काफी पठनीय है।

def parse_block_expression
  return unless expect(:OPEN_BLOCK)
  identifier = parse_identifier
  arguments = parse_arguments
  need(:CLOSE)
  statements = parse_statements
  if expect(:OPEN_INVERSE, :CLOSE)
    inverse_statements = parse_statements
  end
  need(:OPEN_END_BLOCK)
  if identifier.value != parse_identifier.value
    raise("Error. Identifier in closing expression does not match identifier in opening expression")
  end
  need(:CLOSE)
  Magicbars::Nodes::BlockExpression.new(identifier, arguments, statements, inverse_statements)
end

पहला भाग parse_expression विधि के समान है। यह पहचानकर्ता और आर्ग्युमेंट्स के साथ शुरुआती ब्लॉक एक्सप्रेशन को पार्स करता है। बाद में, यह अंदर के ब्लॉक को पार्स करने के लिए parse_statements को कॉल करता है।

एक बार यह हो जाने के बाद, हम {{else}} एक्सप्रेशन की जांच करते हैं, जिसे OPEN_INVERSE टोकन के बाद CLOSE टोकन द्वारा पहचाना जाता है। यदि दोनों टोकन मिलते हैं, तो हम इनवर्स ब्लॉक को पार्स करने के लिए parse_statements को फिर से कॉल करते हैं। अन्यथा, हम उस भाग को पूरी तरह से छोड़ देते हैं।

अंतिम चीज़ के रूप में, हम सुनिश्चित करते हैं कि ओपन ब्लॉक एक्सप्रेशन में उसी पहचानकर्ता का उपयोग करके एक एंड ब्लॉक एक्सप्रेशन है। यदि पहचानकर्ता मेल नहीं खाते, तो हम एक त्रुटि उठाते हैं। अन्यथा, हम एक नया BlockExpression नोड बनाते हैं और इसे लौटाते हैं।

उन्नत ब्लॉक एक्सप्रेशन टेम्पलेट के टोकन के साथ पार्सर को कॉल करने से टेम्पलेट के लिए AST लौटाया जाएगा। मैं यहां उदाहरण आउटपुट शामिल नहीं करूंगा, क्योंकि यह मुश्किल से पठनीय है। इसके बजाय, यहां उत्पन्न AST का एक दृश्य प्रतिनिधित्व है।

क्योंकि हम parse_block_expression के अंदर parse_statements को कॉल कर रहे हैं, ब्लॉक और इनवर्स ब्लॉक दोनों में अधिक एक्सप्रेशन, ब्लॉक एक्सप्रेशन, साथ ही नियमित कंटेंट शामिल हो सकते हैं।

यात्रा जारी है…

हमारी अपनी टेम्पलेटिंग भाषा को लागू करने की दिशा में हमारी यात्रा में हमने अच्छी प्रगति की है। भाषा सिद्धांत में एक संक्षिप्त डुबकी के बाद, हमने अपनी टेम्पलेटिंग भाषा के लिए एक व्याकरण को परिभाषित किया और इसका उपयोग करके स्क्रैच से एक पार्सर को लागू किया।

लेक्सर और पार्सर दोनों के साथ, हम केवल एक इंटरप्रेटर को याद कर रहे हैं जो हमारे टेम्पलेट से इंटरपोलेटेड स्ट्रिंग उत्पन्न करेगा। हम इस भाग को RubyMagic के आगामी संस्करण में कवर करेंगे। जब यह निकले तो अलर्ट पाने के लिए Ruby Magic मेलिंगलिस्ट की सदस्यता लें।

  1. रूबी ऑब्जेक्ट्स को डेकोरेटर पैटर्न से बढ़ाएं: एक व्यावहारिक गाइड रूबी ऑब्जेक्ट्स को डेकोरेटर पैटर्न से बढ़ाएं: एक व्यावहारिक गाइड

    डेकोरेटर डिज़ाइन पैटर्न क्या है? और आप इस पैटर्न का उपयोग अपने रूबी प्रोजेक्ट्स में कैसे कर सकते हैं? डेकोरेटर डिज़ाइन पैटर्न आपको क्लास को बदले बिना नई क्षमताएं जोड़कर एक ऑब्जेक्ट को बढ़ाने में मदद करता है। आइए एक उदाहरण देखें! लॉगिंग और प्रदर्शन इस उदाहरण में हम rest-client जैसे जेम का उपयोग करके

  1. रूबी में Transpose मेथड का उपयोग करके आसानी से पंक्तियों को स्तंभों में बदलें रूबी में Transpose मेथड का उपयोग करके आसानी से पंक्तियों को स्तंभों में बदलें

    आज आप सीखेंगे कि रूबी transpose मेथड का उपयोग करके ग्रिड्स से कैसे निपटें। कल्पना करें कि आपके पास एक परफेक्ट ग्रिड है, मान लीजिए एक 3×3 वर्ग, बहुआयामी सरणी के रूप में। और आप पंक्तियों को लेना और उन्हें स्तंभों में बदलना चाहते हैं। आप ऐसा क्यों करना चाहेंगे? एक उपयोग क्लासिक गेम के लिए है: टिक-टैक-ट

  1. रूबी पार्सिंग में मास्टर करें: स्क्रैच से अपना खुद का पार्सर बनाएं रूबी पार्सिंग में मास्टर करें: स्क्रैच से अपना खुद का पार्सर बनाएं

    पार्सिंग स्ट्रिंग्स के एक समूह को समझने और उन्हें किसी ऐसी चीज़ में बदलने की कला है जिसे हम समझ सकें। आप रेगुलर एक्सप्रेशन्स का उपयोग कर सकते हैं, लेकिन वे हमेशा इस काम के लिए उपयुक्त नहीं होते। उदाहरण के लिए, यह सामान्य ज्ञान है कि HTML को रेगुलर एक्सप्रेशन्स से पार्स करना शायद एक अच्छा विचार नहीं