क्या कार्यात्मक भाषाएं पुनरावृत्ति में बेहतर हैं?


41

टीएल; डीआर: क्या कार्यात्मक भाषाएं गैर-कार्यात्मक लोगों की तुलना में पुनरावृत्ति को बेहतर तरीके से संभालती हैं?

मैं वर्तमान में कोड पूरा 2 पढ़ रहा हूं। पुस्तक के कुछ बिंदु पर, लेखक हमें पुनरावृत्ति के बारे में चेतावनी देता है। उनका कहना है कि जब संभव हो तो इसे टाला जाना चाहिए और लय का उपयोग करके समाधान की तुलना में पुनरावृत्ति का उपयोग करने वाले कार्य आमतौर पर कम प्रभावी होते हैं। एक उदाहरण के रूप में, लेखक ने एक जावा फ़ंक्शन को पुनरावृत्ति का उपयोग करके संख्या की तथ्यात्मक गणना करने के लिए लिखा है, (यह बिल्कुल वैसा ही नहीं हो सकता है क्योंकि मेरे पास इस समय मेरे साथ पुस्तक नहीं है):

public int factorial(int x) {
    if (x <= 0)
        return 1;
    else
        return x * factorial(x - 1);
}

यह एक बुरा समाधान के रूप में प्रस्तुत किया गया है। हालांकि, कार्यात्मक भाषाओं में, पुनरावृत्ति का उपयोग करना अक्सर चीजों को करने का पसंदीदा तरीका है। उदाहरण के लिए, यहाँ पुनरावृत्ति का उपयोग करके हास्केल में फैक्टोरियल फ़ंक्शन है:

factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial (n - 1)

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

तो मेरा प्रश्न मूल रूप से है:

  • क्या कार्यात्मक भाषाएं गैर-कार्यात्मक लोगों की तुलना में पुनरावृत्ति को बेहतर तरीके से संभालती हैं?

संपादित करें: मुझे पता है कि मैंने जिन उदाहरणों का उपयोग किया है, वे मेरे प्रश्न का वर्णन करने के लिए सर्वश्रेष्ठ नहीं हैं। मैं सिर्फ यह बताना चाहता था कि हास्केल (और सामान्य रूप से कार्यात्मक भाषा) गैर-कार्यात्मक भाषाओं की तुलना में बहुत अधिक बार पुनरावृत्ति का उपयोग करता है।


10
बिंदु में मामला: कई कार्यात्मक भाषाएं टेल कॉल ऑप्टिमाइज़ेशन का भारी उपयोग करती हैं, जबकि बहुत कम प्रक्रियात्मक भाषाएं ऐसा करती हैं। इसका मतलब यह है कि उन कार्यात्मक भाषाओं में टेल कॉल पुनरावर्तन बहुत सस्ता है।
जोआचिम सॉयर

7
दरअसल, हास्केल परिभाषा जो आपने दी है वह बहुत खराब है। factorial n = product [1..n]अधिक रसीला है, अधिक कुशल है, और बड़े के लिए स्टैक को अतिप्रवाह नहीं करता है n(और यदि आपको संस्मरण की आवश्यकता है, तो पूरी तरह से विभिन्न विकल्पों की आवश्यकता होती है)। productकुछ के संदर्भ में परिभाषित किया गया है fold, जिसे पुनरावर्ती रूप से परिभाषित किया गया है, लेकिन अत्यधिक देखभाल के साथ। प्रत्यावर्तन है एक स्वीकार्य समाधान समय के सबसे अधिक है, लेकिन यह अभी भी आसान यह गलत / करने से इनकी करना है।

1
@JoachimSauer - थोड़े अलंकरण के साथ, आपकी टिप्पणी एक सार्थक उत्तर देगी।
मार्क बूथ

आपका संपादन इंगित करता है कि आपने मेरा बहाव नहीं पकड़ा है। आपके द्वारा दी गई परिभाषा पुनरावृत्ति का एक आदर्श उदाहरण है जो कार्यात्मक भाषाओं में भी खराब है । मेरा विकल्प भी पुनरावर्ती है (हालांकि यह एक पुस्तकालय समारोह में है) और बहुत ही कुशल है, केवल यह कि यह कैसे पुनरावर्तन करता है इससे फर्क पड़ता है। हास्केल भी एक अजीब मामला है कि आलस सामान्य नियमों को तोड़ता है (बिंदु में मामला: कार्य पूंछ पुनरावर्ती होने के दौरान स्टैक को ओवरफ्लो कर सकता है, और पूंछ पुनरावर्ती होने के बिना बहुत कुशल हो सकता है)।

@delnan: स्पष्टीकरण के लिए धन्यवाद! मैं अपना संपादन संपादित करूंगा;)
मार्को-फिसेट

जवाबों:


36

हां, वे करते हैं, लेकिन केवल इसलिए नहीं कि वे कर सकते हैं , बल्कि इसलिए कि उन्हें करना है

यहां मुख्य अवधारणा शुद्धता है : एक शुद्ध कार्य एक ऐसा फ़ंक्शन है जिसका कोई साइड इफेक्ट नहीं है और कोई राज्य नहीं है। कार्यात्मक प्रोग्रामिंग भाषा आम तौर पर कई कारणों से पवित्रता को गले लगाती है, जैसे कि कोड के बारे में तर्क करना और गैर-स्पष्ट निर्भरता से बचना। कुछ भाषाएँ, विशेष रूप से हास्केल, यहाँ तक कि केवल शुद्ध कोड की अनुमति देने के लिए जाती हैं; किसी भी प्रोग्राम को होने वाले साइड इफेक्ट्स (जैसे प्रदर्शन I / O) को एक गैर-शुद्ध रनटाइम में ले जाया जाता है, जो भाषा को शुद्ध रखता है।

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

हालांकि, साइड इफेक्ट्स न होने का मतलब यह भी है कि पुनरावृत्ति को अधिक कुशलता से लागू किया जा सकता है, और संकलक इसे अधिक आक्रामक तरीके से अनुकूलित कर सकते हैं। मैंने स्वयं ऐसे किसी भी कंपाइलर का गहराई से अध्ययन नहीं किया है, लेकिन जहाँ तक मैं बता सकता हूँ, अधिकांश कार्यात्मक प्रोग्रामिंग लैंग्वेज के कंपाइलर टेल कॉल ऑप्टिमाइज़ेशन करते हैं, और कुछ दृश्यों के पीछे कुछ प्रकार के पुनरावर्ती निर्माणों को भी संकलित कर सकते हैं।


2
रिकॉर्ड के लिए, टेल कॉल एलिमिनेशन शुद्धता पर निर्भर नहीं करता है।
स्कारफ्रिज

2
@ स्कारफ्रिज: बेशक यह नहीं है। हालांकि, जब शुद्धता दी जाती है, तो संकलक के लिए टेल कॉल की अनुमति देने के लिए अपने कोड को फिर से व्यवस्थित करना बहुत आसान होता है।
tdammers

जीसीसी GHC की तुलना में TCO का ज्यादा बेहतर काम करता है, क्योंकि आप TCO को एक थंक के निर्माण में नहीं कर सकते हैं।
dan_waterworth

18

आप पुनरावृत्ति बनाम पुनरावृत्ति की तुलना कर रहे हैं। टेल-कॉल उन्मूलन के बिना , पुनरावृत्ति वास्तव में अधिक कुशल है क्योंकि कोई अतिरिक्त फ़ंक्शन कॉल नहीं है। इसके अलावा, पुनरावृति हमेशा के लिए जा सकती है, जबकि ढेर सारे फ़ंक्शन कॉल से स्टैक स्पेस से बाहर चलना संभव है।

हालांकि, पुनरावृत्ति को एक काउंटर बदलने की आवश्यकता होती है। इसका मतलब है कि एक परिवर्तनशील चर होना चाहिए , जो विशुद्ध रूप से कार्यात्मक सेटिंग में निषिद्ध है। इसलिए कार्यात्मक भाषाओं को विशेष रूप से पुनरावृत्ति की आवश्यकता के बिना संचालित करने के लिए डिज़ाइन किया गया है, इसलिए सुव्यवस्थित फ़ंक्शन कॉल।

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


मुझे पहले से ही पता है कि पैटर्न का मिलान किस बारे में है और मुझे लगता है कि हास्केल में यह एक भयानक विशेषता है जो मुझे उन भाषाओं में याद आती है जिनका मैं उपयोग करता हूं!
मार्को-फिसेट

@marcof मेरा कहना है कि पुनरावृत्ति बनाम पुनरावृत्ति के बारे में सभी बातें आपके कोड नमूने की चिकनाई को संबोधित नहीं करती हैं। यह वास्तव में पैटर्न बनाम कंडिशंस के बारे में है। शायद मुझे अपने उत्तर में ऊपर रखना चाहिए था।
चिरसायकॉक

हां, मैं यह भी समझ गया: पी
मार्को-फिसेट

@chrisaycock: क्या यह पुनरावृत्ति को पूंछ पुनरावृत्ति के रूप में देखना संभव होगा जिसमें लूप बॉडी में उपयोग किए जाने वाले सभी चर दोनों तर्क और पुनरावर्ती कॉल के मान हैं?
जियोर्जियो

@ जियोर्जियो: हाँ, अपने फ़ंक्शन को एक ही प्रकार का एक टपल ले और वापस कर दें।
Ericson2314

5

तकनीकी रूप से नहीं, लेकिन व्यावहारिक रूप से हाँ।

जब आप समस्या के लिए एक कार्यात्मक दृष्टिकोण ले रहे हैं तो पुनरावृत्ति कहीं अधिक सामान्य है। जैसे, एक कार्यात्मक दृष्टिकोण का उपयोग करने के लिए डिज़ाइन की गई भाषाओं में अक्सर ऐसी विशेषताएं शामिल होती हैं जो पुनरावृत्ति को आसान / बेहतर / कम समस्याग्रस्त बनाती हैं। मेरे सिर के ऊपर से, तीन आम हैं:

  1. टेल कॉल ऑप्टिमाइज़ेशन। जैसा कि अन्य पोस्टरों द्वारा बताया गया है, कार्यात्मक भाषाओं को अक्सर TCO की आवश्यकता होती है।

  2. आलसी मूल्यांकन। हास्केल (और कुछ अन्य भाषाओं) का आलसी मूल्यांकन किया जाता है। यह आवश्यक होने तक एक विधि के वास्तविक 'काम' को विलंबित करता है। यह अधिक पुनरावर्ती डेटा संरचनाओं का नेतृत्व करता है और विस्तार से, उन पर काम करने के लिए पुनरावर्ती तरीके।

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

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


1
पुन: 1. प्रारंभिक रिटर्न का टेल कॉल से कोई लेना- देना नहीं है। आप एक टेल कॉल के साथ जल्दी लौट सकते हैं, और "लेट" रिटर्न में भी टेल कॉल की सुविधा हो सकती है, और आपके पास एक सिंपल एक्सप्रेशन हो सकता है जिसमें रीसर्चिव कॉल टेल पोजीशन में हो (cf. OP की फैक्टोरियल परिभाषा)।

@ डेलन: धन्यवाद; यह जल्दी है और जब से मैंने इस चीज का अध्ययन किया है तब से यह काफी समय हो गया है।
तेलस्टाइन

1

हास्केल और अन्य कार्यात्मक भाषाएं आमतौर पर आलसी मूल्यांकन का उपयोग करती हैं। यह सुविधा आपको न खत्म होने वाले पुनरावर्ती कार्यों को लिखने देती है।

यदि आप एक बेस केस को परिभाषित किए बिना एक पुनरावर्ती फ़ंक्शन लिखते हैं जहां पुनरावृत्ति समाप्त हो जाती है, तो आप उस फ़ंक्शन और स्टैकओवरफ़्लो को अनंत कॉल करने के साथ समाप्त होते हैं।

हास्केल भी पुनरावर्ती फ़ंक्शन कॉल अनुकूलन का समर्थन करता है। जावा में प्रत्येक फ़ंक्शन कॉल ओवरहेड और ओवरहेड का कारण होगा।

तो हां, कार्यात्मक भाषाएं दूसरों की तुलना में बेहतर तरीके से पुनरावृत्ति को संभालती हैं।


5
हास्केल बहुत कम गैर-सख्त भाषाओं में से एक है - पूरा एमएल परिवार (कुछ शोध स्पिनॉफ से अलग जो आलस्य को जोड़ता है), सभी लोकप्रिय लिस्प्स, एरलैंग, आदि सभी सख्त हैं। इसके अलावा, दूसरे पैराग्राफ बंद लगता है - के रूप में आप सही ढंग से पहले अनुच्छेद में राज्य, आलस्य करता अनंत प्रत्यावर्तन (हास्केल प्रस्तावना बेहद उपयोगी है की अनुमति देता है forever a = a >> forever aउदाहरण के लिए)।

@ दीनीन: जहां तक ​​मुझे पता है SML / NJ भी आलसी मूल्यांकन प्रदान करता है, लेकिन यह SML के लिए एक अतिरिक्त है। मैं कुछ आलसी कार्यात्मक भाषाओं में से दो का नाम भी रखना चाहता था: मिरांडा और क्लीन।
जियोर्जियो

1

एकमात्र तकनीकी कारण जो मुझे पता है वह यह है कि कुछ कार्यात्मक भाषाएं (और कुछ अनिवार्य भाषाएं यदि मुझे याद हैं) जिसे टेल कॉल ऑप्टिमाइज़ेशन कहा जाता है, जो हर पुनरावर्ती कॉल (यानी पुनरावर्ती) के साथ स्टैक के आकार को बढ़ाने की अनुमति नहीं देता है। अधिक-या-कम स्टैक पर वर्तमान कॉल को बदल देता है)।

ध्यान दें, कि यह अनुकूलन किसी भी पुनरावर्ती कॉल पर काम नहीं करता है , केवल टेल-कॉल पुनरावर्ती तरीके (जैसे। वे तरीके जो पुनरावर्ती कॉल के समय राज्य को बनाए नहीं रखते हैं)


1
(1) इस तरह के अनुकूलन केवल बहुत विशिष्ट मामलों में लागू होते हैं - ओपी का उदाहरण उन्हें नहीं है, और कई अन्य सीधे कार्यों को पूंछ-पुनरावर्ती बनने के लिए कुछ अतिरिक्त देखभाल की आवश्यकता होती है। (2) रियल टेल-कॉल ऑप्टिमाइज़ेशन केवल पुनरावर्ती कार्यों का अनुकूलन नहीं करता है, यह किसी भी कॉल से अंतरिक्ष ओवरहेड को हटा देता है जो तुरंत एक वापसी के बाद होता है।

@ डलेन: (1) हां, बहुत सही। इस उत्तर के मेरे 'मूल मसौदे' में, मैंने उल्लेख किया था कि :( (2) हां, लेकिन प्रश्न के संदर्भ में, मैंने सोचा कि उल्लेख करने के लिए बाहरी होगा।
स्टीवन एवर्स

हां, (2) सिर्फ एक उपयोगी जोड़ है (हालांकि निरंतरता-गुजर शैली के लिए अपरिहार्य), जवाबों का उल्लेख नहीं है।

1

आप गार्बेज कलेक्शन को देखना चाहते हैं , फास्ट है, लेकिन स्टैक फास्टर है , सी प्रोग्रामर का उपयोग करने के बारे में एक पेपर, संकलित सी में स्टैक फ्रेम के लिए "हीप" के रूप में सोचेंगे। मेरा मानना ​​है कि लेखक ने Gcc के साथ छेड़छाड़ की थी। । यह एक निश्चित जवाब नहीं है, लेकिन यह आपको कुछ मुद्दों को पुनरावृत्ति के साथ समझने में मदद कर सकता है।

Alef प्रोग्रामिंग भाषा है, जो योजना 9 बेल लेबोरेटरीज से साथ आने के लिए प्रयोग किया जाता है, एक "बन" बयान (देखें खंड के 6.6.4 था इस संदर्भ )। यह स्पष्ट पूंछ-कॉल पुनरावृत्ति अनुकूलन का एक प्रकार है। "लेकिन यह कॉल स्टैक का उपयोग करता है!" पुनरावृत्ति के विरुद्ध तर्क को संभवतः दूर किया जा सकता है।


0

TL; DR: हाँ वे करते हैं
पुनरावर्तन कार्यात्मक प्रोग्रामिंग में एक महत्वपूर्ण उपकरण है और इसलिए इन कॉलों को अनुकूलित करने पर बहुत काम किया गया है। उदाहरण के लिए, R5RS को (कल्पना में!) की आवश्यकता है कि सभी कार्यान्वयन बिना प्रोग्रामर के अनबाउंड टेल रिकर्सन कॉल को हैंडल करते हैं जो स्टैक ओवरफ्लो के बारे में चिंता करते हैं। तुलना के लिए, डिफ़ॉल्ट रूप से सी कंपाइलर एक स्पष्ट टेल-कॉल ऑप्टिमाइज़ेशन भी नहीं करेगा (लिंक की गई सूची के एक पुनरावर्ती रिवर्स का प्रयास करें) और कुछ कॉल के बाद, प्रोग्राम समाप्त हो जाएगा (कंपाइलर ऑप्टिमाइज़ करेगा, हालांकि, यदि आप उपयोग करते हैं - O2)।

बेशक, ऐसे कार्यक्रमों में जो बुरी तरह से लिखे गए हैं, जैसे कि प्रसिद्ध fibउदाहरण जो घातीय है, संकलक के पास इसके 'जादू' करने के लिए कोई विकल्प नहीं है। इसलिए ध्यान रखा जाना चाहिए कि अनुकूलन में संकलक के प्रयासों में बाधा न आए।

संपादित करें: फ़ॉइल उदाहरण से, मेरा मतलब निम्नलिखित है:

(define (fib n)
 (if (< n 3) 1 
  (+ (fib (- n 1)) (fib (- n 2)))
 )
)

0

कार्यात्मक भाषाएं दो बहुत विशिष्ट प्रकार की पुनरावृत्ति में बेहतर हैं: पूंछ पुनरावृत्ति और अनंत पुनरावर्तन। वे अन्य भाषाओं की तरह ही आपके factorialउदाहरण की तरह, अन्य भाषाओं की तरह ही खराब हैं ।

यह कहना नहीं है कि दोनों प्रतिमानों में नियमित पुनरावृत्ति के साथ काम करने वाले एल्गोरिदम नहीं हैं। उदाहरण के लिए, किसी भी चीज के लिए एक स्टैक जैसी डेटा संरचना की आवश्यकता होती है, जैसे कि गहराई-पहले पेड़ की खोज, पुनरावृत्ति के साथ लागू करना सबसे सरल है।

फ़ंक्शनल प्रोग्रामिंग के साथ रिकर्सियन अधिक बार आता है, लेकिन यह भी अधिक उपयोग किया जाता है, विशेष रूप से शुरुआती लोगों के लिए या ट्यूटोरियल में, शायद इसलिए कि फ़ंक्शनल प्रोग्रामिंग के अधिकांश शुरुआती ने अनिवार्य प्रोग्रामिंग में पहले ही पुनरावृत्ति का उपयोग किया है। अन्य कार्यात्मक प्रोग्रामिंग निर्माण हैं, जैसे सूची समझ, उच्च-क्रम के कार्य, और संग्रह पर अन्य संचालन, जो आमतौर पर वैचारिक रूप से, शैली के लिए, दक्षता के लिए, और अनुकूलन करने की क्षमता के लिए बहुत बेहतर अनुकूल हैं।

उदाहरण के लिए, डेलान का सुझाव factorial n = product [1..n]केवल अधिक संक्षिप्त और पढ़ने में आसान नहीं है, यह अत्यधिक समानांतर भी है। एक का उपयोग करने के लिए एक ही है foldया reduceयदि आपकी भाषा में productपहले से ही निर्मित नहीं है, तो पुनरावृत्ति इस समस्या के लिए अंतिम उपाय का समाधान है। मुख्य कारण जो आप इसे ट्यूटोरियल में पुनरावर्ती रूप से हल करते हैं, वह बेहतर समाधान प्राप्त करने से पहले एक जंपिंग पॉइंट के रूप में है, न कि एक सर्वोत्तम अभ्यास के उदाहरण के रूप में।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.