हमें उम्मीद है कि आपने अपने कॉफी के ऊपर अपने स्ट्रॉपवाफेल्स गर्म कर लिए हैं क्योंकि आज हम चिपचिपे स्ट्रॉप (वह सिरप जो स्ट्रॉपवाफेल के दो हिस्सों को एक साथ चिपकाता है) के साथ चीजों को जोड़ रहे हैं। हमारी श्रृंखला के पहले दो भागों में, हमने एक लेक्सर और एक पार्सर बनाया और अब, हम इंटरप्रेटर जोड़ रहे हैं और उन पर स्ट्रॉप डालकर चीजों को एक साथ जोड़ रहे हैं।
सामग्री
ठीक है! आइए बेकिंग के लिए रसोई तैयार करें और अपनी सामग्री टेबल पर रखें। हमारे इंटरप्रेटर को अपना काम करने के लिए दो सामग्री या जानकारी के टुकड़ों की आवश्यकता होती है: पहले से उत्पन्न Abstract Syntax Tree (AST) और वह डेटा जिसे हम टेम्पलेट में एम्बेड करना चाहते हैं। हम इस डेटा को environment कहेंगे।
AST को पार करने के लिए, हम विज़िटर पैटर्न का उपयोग करके इंटरप्रेटर को लागू करेंगे। एक विज़िटर (और इसलिए हमारा इंटरप्रेटर) एक सामान्य visit विधि लागू करता है जो पैरामीटर के रूप में एक नोड को स्वीकार करता है, इस नोड को संसाधित करता है और संभावित रूप से नोड के कुछ (या सभी) बच्चों के साथ 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 विधि भी बनाएं जो एक टेम्पलेट और एक environment स्वीकार करती है और रेंडर किया गया टेम्पलेट आउटपुट करती है।
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। ऐसा करने के लिए, हम बस अपने visit विधि के अंदर एक विशाल case स्टेटमेंट डाल सकते हैं। हालांकि, यह बहुत जल्दी अपठनीय हो जाएगा। इसके बजाय, आइए अपने कोड को थोड़ा अधिक व्यवस्थित और पठनीय रखने के लिए 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 विधि को यह जानने के लिए कि environment से कौन सी कुंजी लानी है, आइए 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 नोड के समान है, लेकिन मान के आधार पर यह या तो BlockExpression नोड के statements या inverse_statements को संसाधित करना जारी रखता है।
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
यह लगभग सही दिखता है। लेकिन करीब से देखने पर, हम देखते हैं कि संदेश हमें मेलिंग सूची के लिए साइन अप करने के लिए प्रेरित कर रहा है, भले ही हमने environment में subscribed: true प्रदान किया हो। यह सही नहीं लगता…
हेल्पर विधियों के लिए समर्थन जोड़ना
टेम्पलेट पर वापस देखते हुए, हम देखते हैं कि ब्लॉक एक्सप्रेशन में एक if है। environment में subscribed के मान को देखने के बजाय, visit_BlockExpression if के मान को देख रहा है। चूंकि यह environment में मौजूद नहीं है, कॉल nil लौटाता है, जो false है।
हम यहां रुक सकते हैं और घोषणा कर सकते हैं कि हम हैंडलबार्स की नकल नहीं बल्कि 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
हम इन हेल्पर्स को लागू करने के लिए सरल lambdas का उपयोग करते हैं और उन्हें एक हैश में स्टोर करते हैं ताकि हम उन्हें उनके नाम से देख सकें।
अब, आइए visit_Expression को संशोधित करें ताकि environment में मान देखने से पहले हेल्पर लुकअप किया जाए।
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 हेल्पर को लागू करें। आर्ग्युमेंट्स के अलावा, हम उन्हें दो lambdas पास करेंगे ताकि वे तय कर सकें कि हमें नोड के 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 के साथ हमने जो किया उसके समान हैं, केवल इस बार, हम दो lambdas भी पास करते हैं।
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
केवल सतह को खरोंचना
इस तीन-भाग की श्रृंखला में, हमने एक टेम्पलेटिंग भाषा बनाने की मूल बातें शामिल कीं। इन अवधारणाओं का उपयोग इंटरप्रेटेड प्रोग्रामिंग भाषाओं (जैसे Ruby) बनाने के लिए भी किया जा सकता है। स्वीकार करते हैं, हमने कुछ चीजों को छोड़ दिया (उचित त्रुटि प्रबंधन की तरह 🙀) और आज की प्रोग्रामिंग भाषाओं की नींव की केवल सतह को खरोंचा।
हमें उम्मीद है कि आपने श्रृंखला का आनंद लिया और यदि आप इससे अधिक चाहते हैं, तो Ruby Magic सूची की सदस्यता लें। यदि आप अब स्ट्रॉपवाफेल्स के लिए भूखे हैं, तो हमें एक लाइन ड्रॉप करें और हम आपको उनसे ईंधन देने में सक्षम हो सकते हैं!