मेरे पास स्ट्रीम API के शुरुआती डिज़ाइन से कुछ याद हैं जो डिज़ाइन तर्क पर कुछ प्रकाश डाल सकते हैं।
2012 में वापस, हम लैम्ब्डा को भाषा में जोड़ रहे थे, और हम एक संग्रह-उन्मुख या "बल्क डेटा" ऑपरेशंस का सेट चाहते थे, जिसे लैम्ब्डा का उपयोग करके क्रमादेशित किया गया, जो समानता की सुविधा प्रदान करेगा। इस बिंदु द्वारा एक साथ lazily संचालन का विचार अच्छी तरह से स्थापित किया गया था। हम परिणामों को संग्रहीत करने के लिए मध्यवर्ती संचालन भी नहीं चाहते थे।
हमें जिन मुख्य मुद्दों पर निर्णय लेने की आवश्यकता थी, वे थे कि श्रृंखला की वस्तुएं एपीआई में कैसी दिखती थीं और उन्होंने डेटा स्रोतों को कैसे झुका दिया। स्रोत अक्सर संग्रह होते थे, लेकिन हम एक फ़ाइल या नेटवर्क से आने वाले डेटा का समर्थन करना चाहते थे, या एक यादृच्छिक संख्या जनरेटर से, जैसे-पर-मक्खी उत्पन्न डेटा।
डिजाइन पर मौजूदा काम के कई प्रभाव थे। अधिक प्रभावशाली लोगों में Google का अमरूद पुस्तकालय और स्काला संग्रह पुस्तकालय थे। (यदि कोई भी अमरूद के प्रभाव के बारे में आश्चर्यचकित है, तो ध्यान दें कि केविन बॉरिलिन , अमरूद के मुख्य विकासकर्ता थे JSR-335 लैंबडा विशेषज्ञ समूह में थे।) स्काला संग्रह में, हमने मार्टिन ओडस्की की इस बात को विशेष रुचि के रूप में पाया: भविष्य- प्रूफिंग स्काला कलेक्शंस: म्यूटेबल से पर्सेंटेज टू पैरेलल । (स्टैनफोर्ड EE380, 2011 1 जून)
उस समय हमारा प्रोटोटाइप डिजाइन चारों ओर आधारित था Iterable
। परिचित ऑपरेशन filter
, map
और इसके बाद के विस्तार (डिफ़ॉल्ट) तरीके थे Iterable
। कॉलिंग ने एक श्रृंखला में एक ऑपरेशन जोड़ा और दूसरे को वापस कर दियाIterable
। एक टर्मिनल ऑपरेशन की तरह श्रृंखला को स्रोत तक count
बुलाया जाएगा iterator()
, और संचालन प्रत्येक चरण के Iterator के भीतर लागू किया गया था।
चूंकि ये Iterables हैं, आप कॉल कर सकते हैं iterator()
विधि को एक से अधिक बार । फिर क्या होना चाहिए?
यदि स्रोत एक संग्रह है, तो यह ज्यादातर ठीक काम करता है। संग्रह Iterable हैं, और प्रत्येक कॉल iterator()
एक अलग Iterator इंस्टेंस बनाने के लिए है जो किसी भी अन्य सक्रिय उदाहरणों से स्वतंत्र है, और प्रत्येक संग्रह को स्वतंत्र रूप से ट्रैवर्स करता है। महान।
अब क्या होगा अगर स्रोत एक-शॉट है, जैसे फ़ाइल से लाइनें पढ़ना? हो सकता है कि पहले Iterator को सभी मान प्राप्त हों, लेकिन दूसरा और बाद वाला खाली होना चाहिए। हो सकता है कि मूल्यों को इटरेटर के बीच इंटरलेय किया जाना चाहिए। या हो सकता है कि प्रत्येक Iterator को सभी समान मान मिले। फिर, क्या होगा यदि आपके पास दो पुनरावृत्तियाँ हों और एक दूसरे से आगे निकल जाए? जब तक वे पढ़ते हैं तब तक किसी को दूसरे Iterator में मानों को बफर करना होगा। इससे भी बदतर, क्या होगा यदि आप एक Iterator प्राप्त करें और सभी मान पढ़ें, और उसके बाद ही दूसरा Iterator प्राप्त करें। अब मूल्य कहां से आते हैं? क्या उन सभी के लिए एक आवश्यकता है कि वे बफर्ड बनें बस में मामला किसी एक दूसरे इटरेटर चाहता है?
स्पष्ट रूप से, एक-शॉट स्रोत पर कई Iterators की अनुमति देने से बहुत सारे प्रश्न उठते हैं। हमारे पास उनके लिए अच्छे उत्तर नहीं थे। यदि आप कॉल करते हैं तो क्या होता है, इसके लिए हम लगातार, पूर्वानुमानित व्यवहार चाहते थेiterator()
दो बार । इसने हमें कई ट्रैवर्स को रोकने की ओर धकेल दिया, जिससे पाइपलाइनों को एक-शॉट बना दिया गया।
हमने अन्य लोगों को इन मुद्दों पर टकराते हुए भी देखा। JDK में, अधिकांश Iterables संग्रह या संग्रह जैसी वस्तुएं हैं, जो कई ट्रैवर्सल की अनुमति देती हैं। यह कहीं भी निर्दिष्ट नहीं है, लेकिन एक अलिखित उम्मीद थी कि Iterables एकाधिक ट्रैवर्सल की अनुमति देता है। एक उल्लेखनीय अपवाद NIO DirectoryStream है इंटरफ़ेस है। इसकी विशिष्टता में यह दिलचस्प चेतावनी शामिल है:
जबकि DirectoryStream Iterable का विस्तार करता है, यह एक सामान्य उद्देश्य Iterable नहीं है क्योंकि यह केवल एक Iterator का समर्थन करता है; दूसरे या बाद के पुनरावृत्ति प्राप्त करने के लिए पुनरावृत्ति विधि को लागू करना IllegalStateException को फेंकता है।
[मूल में बोल्ड]
यह काफी असामान्य और अप्रिय लग रहा था कि हम नए Iterables का एक पूरा गुच्छा नहीं बनाना चाहते थे जो एक बार ही हो सकता है। इसने हमें Iterable का उपयोग करने से दूर कर दिया।
इस समय के बारे में, ब्रूस एकेल के एक लेख में दिखाई दिया कि उन्होंने स्काला के साथ होने वाली परेशानी का वर्णन किया है। उन्होंने यह कोड लिखा होगा:
// Scala
val lines = fromString(data).getLines
val registrants = lines.map(Registrant)
registrants.foreach(println)
registrants.foreach(println)
यह बहुत सीधा है। यह Registrant
वस्तुओं में पाठ की पंक्तियों को पार करता है और उन्हें दो बार प्रिंट करता है। सिवाय इसके कि यह वास्तव में केवल एक बार उन्हें प्रिंट करता है। यह पता चला है कि उसने सोचा कि registrants
यह एक संग्रह था, जब वास्तव में यह एक पुनरावृत्त है। foreach
एक खाली पुनरावृत्ति का सामना करने के लिए दूसरी कॉल , जिसमें से सभी मान समाप्त हो गए हैं, इसलिए यह कुछ भी प्रिंट नहीं करता है।
इस तरह के अनुभव ने हमें आश्वस्त किया कि यदि कई ट्रैवर्सल का प्रयास किया जाता है, तो स्पष्ट रूप से अनुमानित परिणाम होना बहुत महत्वपूर्ण था। इसने डेटा संग्रह करने वाले वास्तविक संग्रहों से आलसी पाइपलाइन जैसी संरचनाओं के बीच अंतर करने के महत्व पर भी प्रकाश डाला। यह बदले में नए स्ट्रीम इंटरफ़ेस में आलसी पाइपलाइन संचालन को अलग करने और केवल उत्सुक, म्यूटेटिव संचालन को सीधे संग्रह में रखता है। ब्रायन गोएट्ज ने इसके लिए औचित्य समझाया है।
संग्रह-आधारित पाइपलाइनों के लिए कई ट्रैवर्सल की अनुमति देने के बारे में क्या है लेकिन इसे गैर-संग्रह-आधारित पाइपलाइनों के लिए अस्वीकार करना है? यह असंगत है, लेकिन यह समझदार है। यदि आप नेटवर्क से मान पढ़ रहे हैं, तो अवश्य आप उन्हें फिर से नहीं बदल सकते। यदि आप उन्हें कई बार पार करना चाहते हैं, तो आपको उन्हें एक संग्रह में स्पष्ट रूप से खींचना होगा।
लेकिन चलो संग्रह-आधारित पाइपलाइनों से कई ट्रैवर्सल की अनुमति दें। मान लीजिए कि आपने ऐसा किया:
Iterable<?> it = source.filter(...).map(...).filter(...).map(...);
it.into(dest1);
it.into(dest2);
( into
ऑपरेशन अब वर्तनी है collect(toList())
।)
यदि स्रोत एक संग्रह है, तो पहली into()
कॉल स्रोत में वापस Iterators की एक श्रृंखला बनाएगी, पाइपलाइन संचालन को निष्पादित करेगी और परिणामों को गंतव्य में भेज देगी। दूसरी कॉल into()
Iterators की एक और श्रृंखला बना सकती है और पाइपलाइन संचालन को फिर से निष्पादित कर सकती है । यह स्पष्ट रूप से गलत नहीं है, लेकिन इसमें प्रत्येक तत्व के लिए दूसरी बार सभी फ़िल्टर और मानचित्र संचालन करने का प्रभाव है। मुझे लगता है कि कई प्रोग्रामर इस व्यवहार से हैरान रह गए होंगे।
जैसा कि मैंने ऊपर उल्लेख किया है, हम अमरूद डेवलपर्स से बात कर रहे थे। उनके पास एक बढ़िया चीज़ है आइडिया ग्रेवयार्ड जहां वे उन विशेषताओं का वर्णन करते हैं जो उन्होंने कारणों के साथ लागू नहीं करने का फैसला किया । आलसी संग्रह का विचार बहुत अच्छा लगता है, लेकिन यहां उनके बारे में क्या कहना है। एक List.filter()
ऑपरेशन पर विचार करें जो एक रिटर्न देता है List
:
यहां सबसे बड़ी चिंता यह है कि बहुत सारे ऑपरेशन महंगे, रैखिक-समय के प्रस्ताव बन जाते हैं। यदि आप किसी सूची को फ़िल्टर करना चाहते हैं और एक सूची वापस प्राप्त करना चाहते हैं, और केवल एक संग्रह या एक Iterable नहीं है, तो आप उपयोग कर सकते हैं ImmutableList.copyOf(Iterables.filter(list, predicate))
, जो "सामने बताता है" यह क्या कर रहा है और यह कितना महंगा है।
एक विशिष्ट उदाहरण लेने के लिए, सूची पर get(0)
या उसकी लागत क्या है size()
? आमतौर पर इस्तेमाल की जाने वाली कक्षाओं के लिए ArrayList
, वे O (1) हैं। लेकिन अगर आप इनमें से किसी एक को लाज़िली-फ़िल्टर्ड सूची में कहते हैं, तो उसे फ़िल्टरिंग को बैकिंग सूची पर चलाना होगा, और अचानक ये सभी ऑपरेशन O (n) हैं। इससे भी बदतर, यह है कि हर ऑपरेशन पर समर्थन सूची को पीछे छोड़ना पड़ता है ।
यह हमें बहुत आलसी लग रहा था । कुछ कार्यों को सेट करना और वास्तविक निष्पादन को स्थगित करना है जब तक कि आप "गो" न करें। यह इस तरह से चीजों को स्थापित करने के लिए एक और है जो संभावित रूप से बड़ी मात्रा में पुनर्संयोजन को छुपाता है।
गैर-रैखिक या "नो-रीयूज़" धाराओं को अस्वीकार करने के प्रस्ताव में, पॉल सैंडोज़ ने उन्हें "अप्रत्याशित या भ्रमित करने वाले परिणामों" को जन्म देने के रूप में अनुमति देने के संभावित परिणामों का वर्णन किया । उन्होंने यह भी उल्लेख किया कि समानांतर निष्पादन चीजों को और भी पेचीदा बना देगा। अंत में, मैं जोड़ूंगा कि साइड इफेक्ट के साथ एक पाइपलाइन ऑपरेशन मुश्किल और अस्पष्ट बग पैदा करेगा यदि ऑपरेशन को अप्रत्याशित रूप से कई बार निष्पादित किया गया था, या प्रोग्रामर की अपेक्षा कम से कम एक अलग संख्या में। (लेकिन जावा प्रोग्रामर साइड इफेक्ट्स के साथ लैम्ब्डा एक्सप्रेशन नहीं लिखते हैं, क्या वे करते हैं?
तो यह जावा 8 स्ट्रीम एपीआई डिजाइन के लिए मूल तर्क है जो एक-शॉट ट्रैवर्सल की अनुमति देता है और इसके लिए कड़ाई से रैखिक (कोई शाखा नहीं) पाइपलाइन की आवश्यकता होती है। यह कई अलग-अलग स्ट्रीम स्रोतों में निरंतर व्यवहार प्रदान करता है, यह स्पष्ट रूप से आलसी को उत्सुक संचालन से अलग करता है, और यह एक सीधा निष्पादन मॉडल प्रदान करता है।
के संबंध में IEnumerable
, मैं C # और .NET के एक विशेषज्ञ से बहुत दूर हूं, इसलिए यदि मैं कोई गलत निष्कर्ष निकालता हूं, तो मुझे सुधारा जाना (धीरे से) सही होगा। हालांकि, यह प्रकट होता है, जो IEnumerable
कई ट्रैवर्सल को अलग-अलग स्रोतों के साथ अलग-अलग व्यवहार करने की अनुमति देता है; और यह नेस्टेड IEnumerable
संचालन की एक शाखा संरचना की अनुमति देता है , जिसके परिणामस्वरूप कुछ महत्वपूर्ण पुनर्संयोजन हो सकता है। जबकि मैं सराहना करता हूं कि विभिन्न सिस्टम अलग-अलग ट्रेडऑफ बनाते हैं, ये दो विशेषताएं हैं जो हमने जावा 8 स्ट्रीम एपीआई के डिजाइन में बचने की मांग की थीं।
ओपी द्वारा दिया गया क्विकसॉर्ट उदाहरण दिलचस्प है, हैरान करने वाला है, और मुझे यह कहते हुए खेद है, कुछ भयावह है। कॉलिंग QuickSort
एक लेता है IEnumerable
और एक रिटर्न देता है IEnumerable
, इसलिए कोई भी छंटनी वास्तव में तब तक नहीं की जाती है जब तक कि अंतिम IEnumerable
ट्रैवर्स न हो जाए। हालांकि, कॉल क्या करता है, इसका निर्माण एक पेड़ की संरचना है, IEnumerables
जो विभाजन को दर्शाता है कि एस्कॉर्ट वास्तव में ऐसा नहीं कर रहा है। (यह आलसी संगणना है, सब के बाद।) यदि स्रोत में एन तत्व हैं, तो पेड़ अपने व्यापक स्तर पर एन तत्व चौड़ा होगा, और यह गहरा (एन) स्तर होगा।
यह मुझे लगता है - और एक बार फिर, मैं सी # या .NET विशेषज्ञ नहीं हूं - कि यह कुछ सहज-दिखने वाली कॉल का कारण होगा, जैसे कि पिवट चयन ints.First()
, वे देखने के मुकाबले अधिक महंगा होना। पहले स्तर पर, निश्चित रूप से, यह ओ (1) है। लेकिन पेड़ में एक विभाजन पर विचार करें, दाएं हाथ के किनारे पर। इस विभाजन के पहले तत्व की गणना करने के लिए, पूरे स्रोत का पता लगाया जाना चाहिए, एक O (N) ऑपरेशन। लेकिन चूँकि ऊपर के विभाजन आलसी हैं, इसलिए उन्हें ओ (lg N) तुलनाओं की आवश्यकता होती है, उन्हें पुन: प्रतिष्ठित किया जाना चाहिए। तो धुरी का चयन करना एक O (N lg N) ऑपरेशन होगा, जो एक पूरे सॉर्ट की तरह महंगा है।
लेकिन हम वास्तव में तब तक नहीं सुलझते जब तक कि हम वापस लौटे नहीं IEnumerable
। मानक क्विकसॉर्ट एल्गोरिथ्म में, विभाजन का प्रत्येक स्तर विभाजन की संख्या को दोगुना कर देता है। प्रत्येक विभाजन केवल आधा आकार है, इसलिए प्रत्येक स्तर O (N) जटिलता पर रहता है। विभाजन का वृक्ष हे (lg N) ऊँचा है, इसलिए कुल कार्य O (N lg N) है।
आलसी IEnumerables के पेड़ के साथ, पेड़ के नीचे N विभाजन हैं। प्रत्येक विभाजन की गणना के लिए एन तत्वों के एक ट्रावेल की आवश्यकता होती है, जिनमें से प्रत्येक को पेड़ की तुलना में एलजी (एन) की आवश्यकता होती है। पेड़ के तल पर सभी विभाजनों की गणना करने के लिए, O (N ^ 2 lg N) तुलना की आवश्यकता होती है।
(क्या यह सही है? मैं शायद ही इस पर विश्वास कर सकता हूं। कोई कृपया मेरे लिए यह जाँच करें।)
किसी भी मामले में, यह वास्तव में अच्छा है कि IEnumerable
इस तरह का उपयोग गणना की जटिल संरचनाओं के निर्माण के लिए किया जा सकता है। लेकिन अगर यह कम्प्यूटेशनल जटिलता को बढ़ाता है जितना मुझे लगता है कि यह करता है, तो ऐसा लगेगा कि इस तरह से प्रोग्रामिंग करना कुछ ऐसा है जिससे बचा जाना चाहिए जब तक कि कोई बहुत सावधान न हो।