हाल ही में, मेरे
Enumerable::Lazy
पैच को माणिक ट्रंक में
अपनाया गया था। और इसका मतलब है कि रूबी 2.0 में हम कर सकते हैं:
a = [1,2,3,4,2,5].lazy.map { |x| x * 10 }.select { |x| x > 30 }
क्यों?
रूबी सुंदर है, और एक अनिवार्य भाषा के रूप में, फिर भी यह हमें सुरुचिपूर्ण "कार्यात्मक" कोड लिखने की अनुमति देती है। उदाहरण के लिए:
data.map(&:split).map(&:reverse)
यह देखने में बहुत अधिक पठनीय है:
data.map { |l| l.split.reverse }
हालांकि, पहला उदाहरण अक्षम है:
data
परिवर्तित
data
, रूबी दो बार डेटा के माध्यम से गुजरता है, एक मध्यवर्ती सरणी पैदा करता है। जब तक डेटा की मात्रा छोटी है, तब तक यह कमी महत्वपूर्ण नहीं है। लेकिन मान लीजिए कि हम एक बड़ी फाइल को पार्स करते हैं:
File.open("text") do |f| f.each.flat_map(&:split).grep(/ruby/) end
इस मामले में, सरणी के माध्यम से एक डबल पास की अनुमति नहीं है। सौभाग्य से,
Enumerable::Lazy
के आगमन के साथ
Enumerable::Lazy
यह अब कोई समस्या नहीं है।
Enumerable::Lazy
class में ओवरराइड किया गया
map
,
select
और कई अन्य फ़ंक्शन के आलसी संस्करण तुरंत गणना नहीं करते हैं। इसके बजाय, वे
Enumerable::Lazy
का एक उदाहरण देते हैं
Enumerable::Lazy
, आपको आलसी गणनाओं की श्रृंखला बनाने की अनुमति देता है।
अद्यतन
अब जब एन्युमरेटर :: आलसी पर अधिकांश काम समाप्त हो गया है, तो मैंने यह परीक्षण करने का निर्णय लिया कि यह वास्तव में किस तरह का लाभ देता है। परिणाम निराशाजनक थे (पहली बार मैंने बेंचमार्क में गलती की - इसलिए नीचे से आशावादी कोमेन्ट)। नियमित रूप से सरणियों पर संचालन की तुलना में औसतन एक आलसी एन्यूमरेटर 4 (!) टाइम्स धीमा है। यह इस तथ्य के कारण है कि जब एक आलसी श्रृंखला की गणना करते हैं, तो एक ब्लॉक आह्वान ऑपरेशन बहुत बार होता है।
Ruby-lang.org पर चर्चा देखें । इसके बावजूद - एक आलसी एन्यूमरेटर के लाभ समान हैं - यह एक सुरुचिपूर्ण और पठनीय कोड है, जबकि धीमेपन के कारण उपयोगकर्ता मामलों की संख्या कम हो जाती है।
युसुके एंडोह द्वारा उद्धृत एक अच्छा उदाहरण:
a = [] Prime.each do |x| next if x % 4 != 3 a << x break if a.size == 10 end
आलस्य का उपयोग करने में बदल जाता है:
Prime.lazy.select {|x| x % 4 == 3 }.take(10).to_a
कब?
यह कहना मुश्किल है कि रूबी में
Enumerator
साथ आलसी कंप्यूटिंग को कैसे लागू किया जा सकता है, यह सोचने वाले पहले व्यक्ति थे। शायद 2008
की यह पोस्ट पहले में से एक थी। कार्यान्वयन का विचार सरल है और एन्यूमरेटर वस्तुओं की उल्लेखनीय संपत्ति पर आधारित है - उन्हें जंजीरों में जोड़ा जा सकता है। तब से,
कई लेख प्रकाशित किए गए हैं , और विचार की व्याख्या करने वाली टिप्पणियों को enumerator.c में जोड़ा गया है।
रूबी-लैंग पर चर्चा तीन साल से अधिक समय तक चली, और आखिरकार
मात्ज़ ने यता हारा को
लागू करने के लिए
मतदान किया।
इसने
Enumerable::lazy
method को प्रस्तावित किया, जो एक
Enumerable::Lazy
में सरणी को "लपेटता" है
Enumerable::Lazy
उदाहरण, जो बदले में, एक आलसी श्रृंखला बनाने के लिए उपयोग किया जाता है। सी के लिए
पैच अनुरोध को आवाज दी गई और मैंने तुरंत कॉल स्वीकार कर लिया। वैसे, मैं हाल ही में कार्यात्मक प्रोग्रामिंग में रुचि रखता था, और रूबी के आंतरिक क्षेत्रों में खुदाई बहुत दिलचस्प थी। नतीजतन, मुझे
पूल का
अनुरोध करने के लिए सम्मानित किया गया था, जो कि थोड़े समय के बाद, कुछ दिनों पहले
अपनाया गया था। वे इसे ट्रंक में भरते हैं और इसे रूबी 2.0 (
रोडमैप देखें ) में जारी किया जाएगा।
कैसे?
एन्यूमरेटर (छोड़ें यदि आप जानते हैं)
उन लोगों के लिए जिन्हें इस बात की जानकारी नहीं है कि एक नियमित एन्यूमरेटर क्या है और इसके साथ क्या खाया जाता है:
enum = [1, 2].each puts enum.next
जैसा कि आप शायद पहले ही अनुमान लगा चुके हैं,
#to_enum
और
#enum_for
Kernel
मॉड्यूल में स्थित हैं, जिसका अर्थ है कि वे किसी भी वस्तु के लिए सुलभ हैं। उदाहरणों को
enumerator.c से लिया जाता है, यदि आप इसे यहाँ देख सकते हैं:
test / ruby / test_enumerator.rb । Enumrator के इंटर्नल शायद एक अलग पोस्ट के लायक हैं, लेकिन यह ध्यान देने योग्य है कि यह सब जादू
next
फाइबर के साथ लागू किया गया है।
आलसी व्यक्ति
यह समझने के लिए कि कैसे असंख्य :: आलसी काम करता है, बस इस कोड को देखें:
module Enumerable class Lazy < Enumerator def initialize(obj) super() do |yielder| obj.each do |val| if block_given? yield(yielder, val) else yielder << val end end end end def map Lazy.new(self) do |yielder, val| yielder << yield(val) end end end end a = Enumerable::Lazy.new([1,2,3]) a = a.map { |x| x * 10 }.map { |x| x - 1 } puts a.next
यह एक विशिष्ट रूबी कार्यान्वयन है जिसे
यूटाका द्वारा प्रस्तावित किया गया है। लेकिन ध्यान दें - इस उदाहरण में मैं विधि के पैरामीटर के रूप में
&block
प्रोक और कॉल
call
कनवर्ट करने के लिए उपयोग नहीं करता हूं। इसके बजाय, हम
each
लिए सीधे ब्लॉक के अंदर
yield
सकते
each
!
block_given?
उम्मीद के मुताबिक काम भी करता है। इसके अलावा, जबकि ब्लॉक के अंदर, हम अभी भी
self
(जैसे
return
) कह सकते हैं। महान, है ना?
इस विषय पर एक अद्भुत पोस्ट Yehuda Katz (
एक और ) से।
कोड स्वयं काफी सरल है, लेकिन आइए बारीकी से देखें: जैसा कि पहले ही उल्लेख किया गया है, मुख्य विचार एक श्रृंखला का निर्माण करना है। इसलिए,
Enumerable::Lazy#map
Enumerable::Lazy
का एक नया उदाहरण बनाता है
Enumerable::Lazy
इसे एक तर्क के रूप में "पिछले" ऑब्जेक्ट को पास करता है। जब
to_a
कहा जाता है (
each
, एक ब्लॉक,
take
, आदि के साथ), गणना की जाती है: रूबी श्रृंखला (छवि 1) की शुरुआत में वापस आती है, मूल वस्तु से अगला मूल्य प्राप्त करता है और इसे बाहर की ओर लौटाता है, आलसी तरीकों के ब्लॉक के साथ क्रमिक रूप से संशोधित करता है (में) उसी क्रम में जिसमें वे "त्रिशंकु" थे)।

अंजीर। 1 आलसी प्रगंडकों की श्रृंखला।
सी पैच
मेरा
सी पैच रूबी कार्यान्वयन को पूरी तरह से दोहराता है। इस तथ्य को छोड़कर कि मैं
lazy_initialize
अंदर
super
सीधे कॉल करने के बजाय
lazy_initialize
मैं एक जनरेटर (
Enumerator::Generator
)
lazy_initialize
हूं और आरंभ करता हूं, जिसे
Enumerator::Generator
तर्क के रूप में पास किया जाता है।
अंतिम पैच में, nobu ने कोड को थोड़ा सा
lazy_init_block_i
: उन्होंने ब्लॉक-फ़ंक्शन के अंदर if-else की स्थिति को दो अलग-अलग तरीकों (
lazy_init_block_i
और
lazy_init_block
) में
lazy_init_block
, और
lazy_initialize
करने के लिए शर्तों को स्थानांतरित कर
lazy_initialize
।
इसके अलावा, रूबी सरणी को एक ब्लॉक फ़ंक्शन के पैरामीटर के रूप में पारित करने के बजाय, यह मापदंडों के साथ एक नियमित सी सरणी का निर्माण करता है। तदनुसार, अब आपको
yielder
और ब्लॉक के अंदर मान प्राप्त करने के लिए
rb_ary_entry
का उपयोग करने की आवश्यकता नहीं है।
उदाहरण के लिए, यह:
static VALUE lazy_map_func(VALUE val, VALUE m, int argc, VALUE *argv) { VALUE result = rb_funcall(rb_block_proc(), id_call, 1, rb_ary_entry(val, 1)); return rb_funcall(rb_ary_entry(val, 0), id_yield, 1, result); }
इस में बदल गया:
static VALUE lazy_map_func(VALUE val, VALUE m, int argc, VALUE *argv) { VALUE result = rb_yield_values2(argc - 1, &argv[1]); return rb_funcall(argv[0], id_yield, 1, result); }
सच कहूँ तो, मैं रूबी इंटर्नल में एक पूर्ण शुरुआत थी, इसलिए इसे काफी तुच्छ पुल अनुरोध लिखने में दो सप्ताह का समय लगा। इसके अलावा, पहली बार में मैंने एक अलग रास्ता अपनाने की कोशिश की: अपने आप में एक प्रक्रिया के रूप में आलसी तरीकों के ब्लॉक को बचाने की कोशिश की।
यहां देखने के लिए पहला क्रेजी पुल अनुरोध । जब
Enumerator#next
को बुलाया गया, तो अगले मूल्य पर एक के बाद एक सभी प्रक्रियाएं "लागू" की गईं। आलसी
map
और
select
ठीक काम किया है, लेकिन जब मैंने
Enumerator#each
करने की कोशिश की, तो मुझे एहसास हुआ कि यह किसी भी तरह (या नहीं?) था।
आप बस यहां कठिन हो गए हैं, इसलिए यदि आप रूबल में कुछ पैच करने की योजना बनाते हैं, तो
महान लेखों का एक
गुच्छा है । एक बोनस के रूप में, हमें आलसी क्यों होना चाहिए, इस बारे में
एक और लेख है ।
निष्कर्ष
अब
Enumerator::Lazy
पास अपने शस्त्रागार
select
,
map
,
reject
,
grep
(पहले पैच द्वारा जोड़ा गया),
flat_map
,
zip
,
take
,
take_while
,
drop
,
drop_while
,
cycle
(हाल ही में जोड़ा गया
शूगो ) है। एपीआई का विकास और चर्चा जारी है, लेकिन अगर आप अभी आलसी होना चाहते हैं, तो बस रूबी को ट्रंक से निकालें और आनंद लें।
नोट:
Enumerable::Lazy
अब Enumerator::Lazy
- एक रूबी उदाहरण में
super()
को इस तरह कहा जाना चाहिए - कोष्ठक के साथ, अन्यथा माणिक स्वचालित रूप से सभी तर्कों को पारित कर देगा
PS यह अंग्रेजी के
मेरे लेख का अनुवाद है।