आइए इस पोस्ट को एक मजेदार छोटे अनुमान खेल के साथ शुरू करें: आपको क्या लगता है कि Ruby एप्लिकेशन में AppSignal द्वारा ट्रैक की जाने वाली सबसे आम त्रुटि क्या है?
यह मानना उचित है कि आप में से कई ने इस प्रश्न का उत्तर NoMethodError के साथ दिया होगा, एक अपवाद जो किसी ऑब्जेक्ट पर एक गैर-मौजूद विधि को कॉल करने के कारण होता है। कभी-कभी, यह विधि नाम में टाइपो के कारण हो सकता है, लेकिन अधिक बार यह गलत प्रकार के ऑब्जेक्ट पर एक विधि को कॉल करने का परिणाम होता है, जो अक्सर एक अप्रत्याशित nil होता है। क्या हम Ruby डेवलपर्स के रूप में ऐसी त्रुटियों की आवृत्ति को कम करने के लिए कुछ कर सकते हैं?
टाइप्स बचाव के लिए?
टेक्स्ट एडिटर या प्रोग्रामिंग भाषा की पसंद को छोड़कर, कुछ विषय टाइप सिस्टम पर चर्चा की तुलना में तेजी से गर्म बहस में बदल सकते हैं। हमारे पास यहां विवरण में जाने का समय नहीं होगा, लेकिन क्रिस स्मिथ का पोस्ट "What To Know Before Debating Type Systems" उस पर उत्कृष्ट काम करता है।
सबसे व्यापक शब्दों में, टाइप सिस्टम को दो मुख्य श्रेणियों में विभाजित किया जा सकता है—स्थिर और गतिशील। जबकि पूर्व समय से पहले होता है (या तो कंपाइलर या एक अलग टूल के माध्यम से), गतिशील टाइप चेकिंग रनटाइम के दौरान होती है, जहां यह अपवादों का कारण बन सकती है यदि वास्तविक प्रकार डेवलपर की अपेक्षाओं के साथ संरेखित नहीं होते हैं।
दोनों दर्शनों के समर्थकों की मजबूत राय है, लेकिन अफसोस, कई गलत धारणाएं भी तैर रही हैं: स्थिर टाइपिंग के लिए बड़े पैमाने पर टाइप एनोटेशन की आवश्यकता नहीं होती—कई आधुनिक कंपाइलर अपने आप प्रकारों का पता लगा सकते हैं, एक प्रक्रिया जिसे "टाइप इन्फरेंस" के रूप में जाना जाता है। दूसरी ओर, गतिशील रूप से टाइप की गई भाषाएं अपने स्थिर रूप से टाइप किए गए समकक्षों की तुलना में काफी अधिक दोष दर प्रदर्शित नहीं करती हैं।
डक टाइपिंग
Ruby स्वयं एक गतिशील रूप से टाइप-चेक की गई भाषा है और "डक टाइपिंग" दृष्टिकोण का पालन करती है:
यदि यह बत्तख की तरह चलता है और बत्तख की तरह क्वैक करता है, तो यह एक बत्तख होना चाहिए।
इसका मतलब है कि Ruby डेवलपर्स आमतौर पर किसी ऑब्जेक्ट के प्रकार के बारे में ज्यादा चिंता नहीं करते हैं, लेकिन यह कि क्या यह कुछ "संदेशों" (या विधियों) का जवाब देता है।
तो Ruby में स्थिर टाइपिंग से क्यों परेशान हों, आप पूछ सकते हैं? हालांकि यह निश्चित रूप से कोई रामबाण नहीं है जो आपके कोड को जादुई रूप से बग-मुक्त बना देगा, यह कुछ लाभ प्रदान करता है:
- सहीता: स्थिर टाइपिंग कुछ वर्गों की बग्स को रोकने में अच्छी है, जैसे ऊपर उल्लिखित
NoMethodError। - टूलिंग: अक्सर, विकास के दौरान स्थिर प्रकार की जानकारी उपलब्ध होने से बेहतर टूलिंग विकल्प मिलते हैं (जैसे IDEs में रीफैक्टरिंग समर्थन, आदि)
- दस्तावेज़ीकरण: कई स्थिर टाइप की गई भाषाओं में बेहतरीन अंतर्निहित दस्तावेज़ीकरण उपकरण होते हैं। हास्केल का Hoogle इसका बहुत प्रभावी ढंग से उपयोग करता है एक खोज इंजन की पेशकश करके जहां फ़ंक्शन को उनके प्रकार के हस्ताक्षर द्वारा देखा जा सकता है।
- प्रदर्शन: कंपाइलर को जितनी अधिक जानकारी उपलब्ध होगी, उतने अधिक प्रदर्शन अनुकूलन संभावित रूप से लागू किए जा सकते हैं।
यह सूची संपूर्ण नहीं है, और इनमें से अधिकांश बिंदुओं के लिए प्रति-उदाहरण मिल सकते हैं, लेकिन उनमें निश्चित रूप से सच्चाई का एक मूल है।
धीरे-धीरे टाइप चेकिंग
हाल के वर्षों में "धीरे-धीरे टाइप चेकिंग" के रूप में संदर्भित एक दृष्टिकोण ने विभिन्न गतिशील रूप से टाइप-चेक की गई भाषाओं में प्रवेश किया है: JS के लिए TypeScript, PHP के लिए Hack, और Python के लिए mypy। इन दृष्टिकोणों में आम बात यह है कि उन्हें ऑल-ऑर-नथिंग दृष्टिकोण की आवश्यकता नहीं होती, बल्कि इसके बजाय, डेवलपर्स को चर और अभिव्यक्तियों में प्रकार की जानकारी को धीरे-धीरे जोड़ने की अनुमति देते हैं जैसा वे फिट देखते हैं। यह मौजूदा बड़े कोडबेस के लिए विशेष रूप से उपयोगी है, जहां कोई सिस्टम के सबसे महत्वपूर्ण भागों को स्थिर रूप से जांच सकता है जबकि बाकी को अनटाइप्ड और रनटाइम पर चेक किया जाता है। Ruby के लिए सभी टाइप चेकिंग समाधान जिन्हें हम इस लेख के बाकी हिस्से में खोजेंगे, उसी दृष्टिकोण का पालन करते हैं।
विकल्प
यह देखने के बाद कि Ruby डेवलपर्स अपने विकास वर्कफ़्लो में स्थिर टाइप चेकिंग क्यों जोड़ना चाहेंगे, वर्तमान में ऐसा करने के लिए कुछ लोकप्रिय विकल्पों का पता लगाने का समय आ गया है। हालांकि, यह ध्यान रखना महत्वपूर्ण है कि Ruby में स्थिर टाइप चेकिंग जोड़ने का विचार नया नहीं है। मैरीलैंड विश्वविद्यालय के शोधकर्ताओं ने 2009 की शुरुआत में डायमंडबैक रूबी (Druby) नामक एक Ruby एक्सटेंशन पर काम किया और टफ्ट्स यूनिवर्सिटी प्रोग्रामिंग लैंग्वेज ग्रुप ने 2013 में द रूबी टाइप चेकर नामक एक पेपर जारी किया, जो अंततः RDL प्रोजेक्ट की ओर ले गया, जो एक लाइब्रेरी के रूप में टाइप चेकिंग और डिज़ाइन-बाय-कॉन्ट्रैक्ट क्षमताएं प्रदान करता है।
Sorbet
Stripe द्वारा विकसित, Sorbet वर्तमान में Ruby के लिए सबसे चर्चित टाइप चेकिंग समाधान है, कम से कम इसलिए नहीं क्योंकि Shopify, GitLab, Kickstarter और Coinbase जैसी बड़ी कंपनियां इसके बंद बीटा चरण के दौरान शुरुआती अपनाने वाली थीं। इसे पिछले साल के Ruby Kaigi के दौरान मूल रूप से घोषित किया गया था और इस साल 20 जून को इसका पहला सार्वजनिक रिलीज़ देखा गया। Sorbet आधुनिक C++ में लिखा गया है और Matz की प्राथमिकताओं के बावजूद (उद्धरण: "I hate type annotations"), टाइप एनोटेशन पर आधारित दृष्टिकोण का विकल्प चुना। Sorbet के बारे में एक विशेष रूप से दिलचस्प बात यह है कि यह स्थिर और गतिशील टाइप चेकिंग के संयोजन का विकल्प चुनता है क्योंकि Ruby की अत्यधिक गतिशील प्रकृति और मेटाप्रोग्रामिंग क्षमताएं स्थिर टाइप सिस्टम के लिए चुनौतीपूर्ण हैं।
# typed: true
class Test
extend T::Sig
sig {params(x: Integer).returns(String)}
def to_s(x)
x.to_s
end
end
टाइप चेकिंग को सक्षम करने के लिए, हमें पहले # typed: true मैजिक कमेंट जोड़ना होगा और अपनी क्लास को T::Sig मॉड्यूल के साथ विस्तारित करना होगा। वास्तविक टाइप एनोटेशन sig विधि के साथ निर्दिष्ट किया जाता है:
sig {params(x: Integer).returns(String)}
जो निर्दिष्ट करता है कि यह विधि x नाम का एक सिंगल आर्ग्युमेंट लेती है जो Integer प्रकार का है और एक String लौटाती है। इस विधि को गलत आर्ग्युमेंट प्रकार के साथ कॉल करने का प्रयास करने से एक त्रुटि होगी:
Test.new.to_s("42") # Expected Integer but found String("42") for argument x
इन बुनियादी जांचों के अलावा, Sorbet के पास अपनी आस्तीन में कुछ और तरकीबें हैं। उदाहरण के लिए, यह हमें nil पर भयानक NoMethodError से बचा सकता है:
users = T::Array[User].new
user = users.first
user.username # Method username does not exist on NilClass component of T.nilable(User)
उपरोक्त स्निपेट User ऑब्जेक्ट्स की एक खाली सरणी को परिभाषित करता है और जब हम पहले तत्व (जो nil लौटाएगा) तक पहुंचने का प्रयास करते हैं तो Sorbet सही ढंग से हमें चेतावनी देता है कि NilClass पर username नाम की कोई विधि उपलब्ध नहीं है। हालांकि, यदि हम निश्चित हैं कि एक निश्चित मान कभी nil नहीं हो सकता, तो हम Sorbet को यह बताने के लिए T.must का उपयोग कर सकते हैं:
users = T::Array[User].new
user = T.must(users.first)
user.username
जबकि उपरोक्त कोड अब टाइप चेक करेगा, यह रनटाइम अपवाद का कारण बन सकता है, इसलिए इस सुविधा का सावधानी से उपयोग करें।
ऐसा बहुत कुछ है जो Sorbet हमारे लिए कर सकता है: डेड कोड डिटेक्शन, टाइप पिनिंग (अनिवार्य रूप से एक चर को एक निश्चित प्रकार के लिए प्रतिबद्ध करना, उदाहरण के लिए, एक बार इसे एक स्ट्रिंग असाइन करने के बाद, इसे कभी भी एक पूर्णांक असाइन नहीं किया जा सकता), या इंटरफेस को परिभाषित करने की क्षमता।
इसके अतिरिक्त, Sorbet "Ruby Interface" फाइलों (rbi) के साथ भी काम कर सकता है जिन्हें यह आपके वर्तमान कार्य निर्देशिका में sorbet/ फ़ोल्डर में रखता है। यह हमें एक प्रोजेक्ट द्वारा उपयोग किए जाने वाले सभी जेम्स के लिए इंटरफ़ेस परिभाषाएं उत्पन्न करने की अनुमति देता है, जो हमें और भी अधिक प्रकार की त्रुटियों को खोजने में मदद कर सकता है।
Sorbet के बारे में एक ही लेख में कवर करने के लिए बहुत कुछ है (जैसे अलग-अलग सख्ती स्तर या मेटाप्रोग्रामिंग प्लगइन्स), लेकिन इसका दस्तावेज़ीकरण पहले से ही बहुत अच्छा है और PRs के लिए खुला है।
Steep
Sorbet का सबसे व्यापक रूप से ज्ञात विकल्प Soutaro Matsumoto द्वारा Steep है। यह एनोटेशन का उपयोग नहीं करता और अपने आप कोई टाइप इन्फरेंस नहीं करता। इसके बजाय, यह पूरी तरह से sig निर्देशिका में .rbi फाइलों पर निर्भर करता है।
आइए निम्न सरल Ruby क्लास से शुरू करें:
class User
attr_reader :first_name, :last_name, :address
def initialize(first_name, last_name, address)
@first_name = first_name
@last_name = last_name
@address = address
end
def full_name
"#{first_name} #{last_name}"
end
end
हम निम्न कमांड के साथ एक प्रारंभिक user.rbi फाइल बना सकते हैं:
$ steep scaffold user.rb > sig/user.rbi
इसके परिणामस्वरूप निम्न फ़ाइल आती है जो एक प्रारंभिक बिंदु के रूप में अभिप्रेत है (इस तथ्य से स्पष्ट है कि सभी प्रकारों को any के रूप में निर्दिष्ट किया गया है, जो कोई सुरक्षा प्रदान नहीं करता):
class User
@first_name: any
@last_name: any
@address: any
def initialize: (any, any, any) -> any
def full_name: () -> String
end
हालांकि, यदि हम इस बिंदु पर टाइप चेक करने का प्रयास करते हैं, तो हमें कुछ त्रुटियों का सामना करना पड़ेगा:
$ steep check
user.rb:11:7: NoMethodError: type=::User, method=first_name (first_name)
user.rb:11:21: NoMethodError: type=::User, method=last_name (last_name)
हम इसे देखने का कारण यह है कि Steep को एक विशेष टिप्पणी की आवश्यकता होती है ताकि वह जान सके कि attr_reader के माध्यम से कौन सी विधियां परिभाषित की गई हैं, तो आइए इसे जोड़ें:
# @dynamic first_name, last_name, address
attr_reader :first_name, :last_name, :address
इसके अतिरिक्त, हमें जनरेट की गई .rbi फाइल में विधियों के लिए परिभाषाएं जोड़नी होंगी। जब हम इस पर हैं, तो आइाए हस्ताक्षरों को any से वास्तविक प्रकारों में भी बदलें:
class User
@first_name: String
@last_name: String
@address: Address
def initialize: (String, String, Address) -> any
def first_name: () -> String
def last_name: () -> String
def address: () -> Address
def full_name: () -> String
end
अब, सब कुछ अपेक्षा के अनुरूप काम करता है और steep check कोई त्रुटि नहीं लौटाता।
हमने अब तक जो देखा है, उसके अलावा, Steep जेनेरिक्स (जैसे Hash) और यूनियन प्रकारों का भी समर्थन करता है, जो कई प्रकारों के बीच एक either-or विकल्प का प्रतिनिधित्व करते हैं। उदाहरण के लिए, एक उपयोगकर्ता की top_post विधि उपयोगकर्ता द्वारा लिखी गई उच्चतम-रैंक वाली पोस्ट लौटा सकती है, या nil यदि उन्होंने अभी तक कुछ भी योगदान नहीं दिया है। इसे यूनियन प्रकार (Post | nil) के माध्यम से दर्शाया गया है, और संबंधित हस्ताक्षर इस तरह दिखेगा:
def top_post: () -> (Post | nil)
जबकि Steep में निश्चित रूप से Sorbet की तुलना में कम विशेषताएं हैं, यह अभी भी एक उपयोगी उपकरण है और ऐसा लगता है कि यह Matz की कल्पना के अनुरूप है कि Ruby 3 में टाइप चेकिंग कैसी दिखनी चाहिए।
Ruby Type Profiler
Yusuke Endoh (Ruby डेवलपर सर्किल में "mame" के रूप में बेहतर जाने जाते हैं) Cookpad से एक तथाकथित लेवल 1 टाइप चेकर जिसे Ruby Type Profiler कहा जाता है, पर काम कर रहे हैं। यहां प्रस्तुत अन्य समाधानों के विपरीत, इसे हस्ताक्षर फाइलों या टाइप एनोटेशन की आवश्यकता नहीं होती, बल्कि यह इसे पार्स करते समय Ruby प्रोग्राम के बारे में जितना संभव हो सके उतना अनुमान लगाने का प्रयास करता है। हालांकि यह Steep या Sorbet की तुलना में बहुत कम संभावित समस्याओं को पकड़ता है, यह डेवलपर के लिए बिना किसी अतिरिक्त लागत के आता है।
सारांश
हालांकि कोई भी भविष्य की भविष्यवाणी नहीं कर सकता, ऐसा लगता है कि Ruby में टाइप चेकिंग कुछ ऐसा है जो यहां रहने वाला है। वर्तमान में, .rbi फाइलों में उपयोग के लिए "Ruby Signature Language" को मानकीकृत करने के प्रयास चल रहे हैं (संभावित रूप से Ruby Type Profiler द्वारा स्कैफोल्डेड), ताकि डेवलपर्स जिस भी टूल को पसंद करें उसका उपयोग कर सकें। Steep पहले से ही लाइब्रेरी लेखकों को अपने जेम्स के साथ टाइप जानकारी शिप करने की अनुमति देता है, और Sorbet के पास sorbet-typed के रूप में एक समान तंत्र है, जो TypeScript परिभाषाओं के लिए DefinitelyTyped रिपॉजिटरी से प्रेरित था। यदि आप Ruby में टाइप चेकिंग के भविष्य को आकार देने में मदद करने में रुचि रखते हैं, तो अब शामिल होने का एक अच्छा समय है!