2012-07-27 26 views
9

स्कैला नौसिखिया के रूप में, मैं किताबें, दस्तावेज़ पढ़ रहा हूं और http://aperiodic.net/phil/scala/s-99/ पर मिली समस्याओं को हल करने का प्रयास करता हूं। ऐसा लगता है कि सही स्कैला कोड समानांतरता को सुरक्षित बनाने और ताले का उपयोग करने से बचने के लिए लूप और चर के बजाए अपरिवर्तनीय मूल्यों (वैल्यू) और रिकर्सन पर आधारित है।स्कैला नौसिखिया: रिकर्सन और स्टैक ओवरफ्लो त्रुटि

उदाहरण के लिए, व्यायाम P22 के लिए एक संभव समाधान (http://aperiodic.net/phil/scala/s-99/p22.scala) है:

// Recursive. 
def rangeRecursive(start: Int, end: Int): List[Int] = 
if (end < start) Nil 
else start :: rangeRecursive(start + 1, end) 

बेशक इस कोड को कॉम्पैक्ट है और स्मार्ट लग रहा है लेकिन निश्चित रूप से, अगर प्रत्यावर्तन की संख्या अधिक है, आप करेंगे एक स्टैक ओवरफ्लो त्रुटि का सामना करें (रेंजक्रुसेसरिव (1,10000) उदाहरण के लिए कोई जेवीएम ट्यूनिंग नहीं है)। यदि आप अंतर्निहित List.range (https://github.com/scala/scala/blob/v2.9.2/src/library/scala/collection/immutable/List.scala#L1) के स्रोत को देखते हैं, तो आप देखेंगे कि लूप और वर्रों का उपयोग किया जाता है।

मेरा सवाल है कि स्केल सीखने वाली सामग्री के प्रभाव को कैसे प्रबंधित किया जाए जो vals और recursion को बढ़ावा दे रहा है यह जानकर कि इस तरह का कोड रिकर्सन की संख्या के कारण टूट सकता है?

+0

स्कैला कंपाइलर [trampoolined] में संकलित पूंछ-रिकर्सिव कॉल बनाने के लिए पर्याप्त स्मार्ट है (http://blog.richdougherty.com/2009/04/tail-calls-tailrec-and-trampolines.html) पूंछ रिकर्सन (JVM टीसीई का समर्थन नहीं करता है), जो स्टैकवॉफ्लो को नहीं लेगा। यदि आप यह सुनिश्चित करना चाहते हैं कि आपका कोड पूंछ रिकर्सिव है, तो विधि हस्ताक्षर –

उत्तर

11

स्कैला के बारे में अच्छी बात यह है कि आप इसमें अपना रास्ता आसान बना सकते हैं। शुरू करना, आप लूप लिख सकते हैं, और भाषा के साथ अधिक आरामदायक होने के साथ रिकर्सन के साथ और अधिक कर सकते हैं। आप क्लोजर या हास्केल जैसी अधिक 'शुद्ध' कार्यात्मक भाषाओं के साथ ऐसा नहीं कर सकते हैं। दूसरे शब्दों में, आप अपरिवर्तनीयता और val के साथ सहज महसूस कर सकते हैं, और बाद में रिकर्सन पर जा सकते हैं।

जब आप रिकर्सन के साथ शुरू करते हैं, तो आपको पूंछ कॉल रिकर्सन देखना चाहिए। यदि रिकर्सिव कॉल फ़ंक्शन में अंतिम कॉल है, तो स्कैला कंपाइलर इसे बाइटकोड में लूप में अनुकूलित करेगा। इस तरह, आपको StackOverflowError एस नहीं मिलेगा। साथ ही, यदि आप अपने रिकर्सिव फ़ंक्शन में @tailrec एनोटेशन जोड़ते हैं, तो संकलक आपको चेतावनी देगा कि आपका फ़ंक्शन पूंछ कॉल रिकर्सिव नहीं है।

उदाहरण के लिए, आपके प्रश्न में फ़ंक्शन पूंछ कॉल रिकर्सिव नहीं है। ऐसा लगता है कि rangeRecursive पर कॉल फ़ंक्शन में आखिरी है, लेकिन जब यह कॉल लौटाता है, तो उसे अभी भी कॉल के परिणामस्वरूप start जोड़ना होगा। इसलिए, यह पूंछ कॉल रिकर्सिव नहीं हो सकता है: कॉल रिटर्न होने पर इसे अभी भी काम करना होगा।

+0

पर @tailrec एनोटेशन जोड़ें, इसलिए अंतिम लक्ष्य पूंछ रिकर्सिव फ़ंक्शंस बनाना है, न केवल रिकर्सिव फ़ंक्शंस, है ना? – Brice

+0

@ ब्रिस जितना संभव हो, हाँ। कभी-कभी यह संभव नहीं है, लेकिन अक्सर, यह है। इन मामलों में, आपको प्रदर्शन सुधार मिलते हैं, और आपको स्टैक ओवरफ़्लो के बारे में चिंता करने की आवश्यकता नहीं होगी। – jqno

1

यदि आप उपरोक्त को फिर से लिखते हैं तो यह पूंछ रिकर्सिव है, संकलक कोड को थोड़ी देर में अनुकूलित करेगा। Addititonally आप @tailrec एनोटेशन का उपयोग त्रुटि प्राप्त करने के लिए कर सकते हैं जब यह एनोटेटिंग विधि पूंछ रिकर्सिव नहीं है। इस प्रकार आपको यह जानने में सक्षम बनाता है कि "जब आपको यह सही लगेगा"।

3

यहां विधि विधि रिकर्सिव बनाने का एक उदाहरण दिया गया है। @tailrec एनोटेशन आवश्यक नहीं है, संकलक इसके बिना अनुकूलित करेगा। लेकिन यह संकलक को एक त्रुटि को ध्वजांकित करता है जब यह अनुकूलन नहीं कर सकता है।

scala> def rangeRecursive(start: Int, end: Int): List[Int] = { 
    | @scala.annotation.tailrec 
    | def inner(accum : List[Int], start : Int) : List[Int] = { 
    |  if (end < start) accum.reverse 
    |  else inner(start :: accum, start + 1) 
    | } 
    | 
    | inner(Nil, start) 
    | } 
rangeRecursive: (start: Int,end: Int)List[Int] 

scala> rangeRecursive(1,10000) 
res1: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,... 

यह एक आम तकनीक "संचायक गुजर शैली" जहां मध्यवर्ती परिणाम संचित और प्रत्यावर्तन के अगले चरण पर पारित कर रहे हैं कहा जाता है का उपयोग करता है। संचित परिणाम लौटने के लिए नीचे सबसे अधिक कदम जिम्मेदार है। इस मामले में संचयक इसके परिणाम को पीछे की ओर बनाने के लिए होता है ताकि मूल मामले को इसे उलट दिया जाए।

+0

रिवर्स का उपयोग न करने के लिए 'आंतरिक (प्रारंभ :: शुरू करें, प्रारंभ + 1)' करने के लिए एक और तरीका है। यह ऐसा हो सकता है: 'आंतरिक (accum ::: सूची (प्रारंभ), प्रारंभ + 1)' मुझे नहीं पता कि शिकायतकर्ता के लिए और अधिक महंगा क्या हो सकता है। –

+0

वैसे मुझे जवाब खुद मिला। प्रदर्शन के मामले में मेरा दूसरा समाधान वास्तव में बुरा है। कोई बात नहीं। –

1

यहाँ, जेम्स Iry के जवाब के लिए एक विकल्प है ही व्यवहार के साथ:

def rangeRecursive(start: Int, end: Int): List[Int] = { 
    def inner(start : Int) : Stream[Int] = { 
     if (end < start) Stream.empty 
     else start #:: inner(start + 1) 
    } 

    inner(start).toList 
} 

scala> rangeRecursive(1,10000) 
res1: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,... 

यह फेंक नहीं है एक StackOverflowError क्योंकि Stream.cons -operator (#::) संदर्भ द्वारा पूंछ संग्रहीत करता है।दूसरे शब्दों में, स्ट्रीम तत्वों की गणना stream.toList तक नहीं की जाती है।

मेरी राय में, इस (बस Stream.empty द्वारा #:: और Nil द्वारा :: की जगह), क्योंकि यह सबसे निकट अनुभवहीन प्रारंभिक एल्गोरिथ्म जैसा दिखता है संचायक पैटर्न की तुलना में अधिक पठनीय है। इसके अलावा, accum.reverse की कोई आवश्यकता नहीं है, जिसे आसानी से भुलाया जा सकता है।

+0

मैं अपने कोड में इस पैटर्न का उपयोग कर रहा हूं, लेकिन मुझे उत्सुकता है कि 'आंतरिक (प्रारंभ) .toist' क्यों एक स्टैक ओवरफ्लो त्रुटि नहीं देता है। => क्योंकि यह एक पुनरावर्ती निर्माण नहीं है? – BlueSky

+0

'# ::' ऑपरेटर दाएं हाथ की तरफ मूल्य के बजाए फ़ंक्शन के रूप में लेता है। 'आंतरिक (प्रारंभ) .toist' सभी मानों के माध्यम से पुनरावृत्ति करने के लिए एक लूप का उपयोग करेगा। अगले मान की गणना उस लूप के प्रत्येक पुनरावृत्ति में की जाती है। –

संबंधित मुद्दे