साइड इफेक्ट्स रेफरेंशियल ट्रांसपेरेंसी ब्रेकिंग


11

स्काला में कार्यात्मक प्रोग्रामिंग, संदर्भात्मक पारदर्शिता को तोड़ने पर एक साइड इफेक्ट के प्रभाव की व्याख्या करता है:

साइड इफेक्ट, जिसका तात्पर्य रेफरल ट्रांसपेरेंसी के कुछ उल्लंघन से है।

मैंने SICP का एक हिस्सा पढ़ा है , जो एक कार्यक्रम का मूल्यांकन करने के लिए "प्रतिस्थापन मॉडल" का उपयोग करने पर चर्चा करता है।

जैसा कि मैं संदर्भात्मक पारदर्शिता (आरटी) के साथ प्रतिस्थापन मॉडल को मोटे तौर पर समझता हूं , आप किसी फ़ंक्शन को उसके सबसे सरल भागों में लिख सकते हैं। यदि अभिव्यक्ति आरटी है, तो आप अभिव्यक्ति को डी-कंपोज कर सकते हैं और हमेशा एक ही परिणाम प्राप्त कर सकते हैं ।

हालांकि, जैसा कि उपरोक्त उद्धरण बताता है, साइड इफेक्ट्स का उपयोग करके प्रतिस्थापन मॉडल को तोड़ / सकता है।

उदाहरण:

val x = foo(50) + bar(10)

यदि fooऔर bar इसके साइड इफेक्ट्स नहीं हैं, तो या तो फ़ंक्शन निष्पादित करना हमेशा उसी परिणाम को लौटाएगा x। लेकिन, अगर उनके साइड इफेक्ट होते हैं, तो वे एक चर को बदल देंगे जो प्रतिस्थापन मॉडल में एक रिंच को बाधित / फेंकता है।

मैं इस स्पष्टीकरण के साथ सहज महसूस करता हूं, लेकिन मैं इसे पूरी तरह से नहीं समझता हूं।

कृपया मुझे सही करें और आरटी को तोड़ने वाले साइड इफेक्ट के संबंध में किसी भी छेद को भरें, साथ ही प्रतिस्थापन मॉडल पर प्रभावों पर चर्चा करें।

जवाबों:


20

आइए संदर्भात्मक पारदर्शिता के लिए एक परिभाषा के साथ शुरू करें :

एक अभिव्यक्ति को संदर्भित रूप से पारदर्शी कहा जाता है यदि इसे किसी प्रोग्राम के व्यवहार को बदलने के बिना इसके मूल्य के साथ बदला जा सकता है (दूसरे शब्दों में, एक प्रोग्राम जो उसी इनपुट पर समान प्रभाव और आउटपुट देता है)।

इसका क्या मतलब है कि (उदाहरण के लिए) आप कार्यक्रम के किसी भी हिस्से में 7 के साथ 2 + 5 को बदल सकते हैं, और कार्यक्रम को अभी भी काम करना चाहिए। इस प्रक्रिया को प्रतिस्थापन कहा जाता है स्थानापन्न मान्य है यदि, और केवल अगर, 2 + 5 को कार्यक्रम के किसी अन्य भाग को प्रभावित किए बिना 7 के साथ बदला जा सकता है ।

मान लीजिए कि मेरे पास एक क्लास है जिसे Bazफंक्शंस Fooऔर उसके साथ बुलाया जाता Barहै। सादगी के लिए, हम बस इतना ही कहेंगे Fooऔर Barदोनों उस मान को वापस लौटा देंगे, जिसमें Foo(2) + Bar(5) == 7आप उम्मीद कर रहे हैं। रेफ़रेंशियल ट्रांसपेरेंसी गारंटी देती है कि आप अपने प्रोग्राम में कहीं भी एक्सप्रेशन के Foo(2) + Bar(5)साथ एक्सप्रेशन को बदल सकते हैं 7, और प्रोग्राम अभी भी समान रूप से कार्य करेगा।

लेकिन क्या हुआ अगर Fooवापस लौटा गया मान, लेकिन Barवापस लौटाया गया मान, और अंतिम मूल्य प्रदान किया गया Foo? यदि आप कक्षा के Fooभीतर किसी स्थानीय चर का मान संग्रहीत करते हैं, तो यह करना काफी आसान है Baz। ठीक है, अगर उस स्थानीय चर का प्रारंभिक मान 0 है, तो अभिव्यक्ति पहली बार आपके द्वारा आह्वान किए Foo(2) + Bar(5)जाने के अपेक्षित मूल्य 7को वापस कर देगी, लेकिन यह 9दूसरी बार जब आप इसे वापस करेंगे ।

यह संदर्भात्मक पारदर्शिता का दो तरह से उल्लंघन करता है। सबसे पहले, बार को उसी अभिव्यक्ति को वापस करने के लिए नहीं गिना जा सकता है जिसे हर बार कहा जाता है। दूसरा, एक साइड-इफेक्ट हुआ है, जिसका अर्थ है कि फू को कॉल करना बार के रिटर्न मूल्य को प्रभावित करता है। चूंकि आप अब गारंटी नहीं दे सकते कि Foo(2) + Bar(5)7 के बराबर होगी, आप अब विकल्प नहीं दे सकते।

यह वही है जो व्यवहार में प्रासंगिक पारदर्शिता का मतलब है; संदर्भित रूप से पारदर्शी फ़ंक्शन कुछ मान को स्वीकार करता है, और प्रोग्राम में अन्य कोड को प्रभावित किए बिना, कुछ इसी मूल्य को वापस करता है, और हमेशा उसी इनपुट को दिए गए समान आउटपुट देता है।


5
तो ब्रेकिंग RTआपको उपयोग करने से रोकता है substitution model.बड़ी समस्या जिसका उपयोग नहीं कर पा रहा substitution modelहै क्या वह प्रोग्राम के बारे में तर्क करने के लिए उपयोग करने की शक्ति है?
केविन मेरेडिथ

यह बिल्कुल सही है।
रॉबर्ट हार्वे

1
+1 शानदार ढंग से स्पष्ट और समझने योग्य उत्तर। धन्यवाद।
रचेत

2
इसके अलावा, अगर वे कार्य पारदर्शी या "शुद्ध" हैं, तो वे जिस क्रम में चलते हैं, वह वास्तव में महत्वपूर्ण नहीं है, हम परवाह नहीं करते हैं कि अगर फू () या बार () पहले चलता है, और कुछ मामलों में वे कभी भी मूल्यांकन नहीं कर सकते हैं यदि उनकी आवश्यकता नहीं है
ज़ाचरी के

1
फिर भी आरटी का एक और फायदा यह है कि महंगे रेफरेंशियल ट्रांसपेरेंट एक्सप्रेशंस को कैश किया जा सकता है (क्योंकि एक या दो बार इनका मूल्यांकन करने के बाद सटीक समान रिजल्ट तैयार करना चाहिए)।
डकैरो

3

कल्पना करें कि आप एक दीवार बनाने की कोशिश कर रहे हैं और आपको विभिन्न आकारों और आकारों में बक्से का वर्गीकरण दिया गया है। आपको दीवार में एक विशेष एल-आकार का छेद भरने की आवश्यकता है; क्या आपको एल-आकार के बॉक्स की तलाश करनी चाहिए या क्या आप उचित आकार के दो सीधे बक्से स्थानापन्न कर सकते हैं?

कार्यात्मक दुनिया में, इसका उत्तर यह है कि या तो समाधान काम करेगा। अपनी कार्यात्मक दुनिया का निर्माण करते समय, आपको कभी यह देखने के लिए बक्से को खोलना नहीं पड़ता है कि क्या है।

अनिवार्य दुनिया में, हर बॉक्स की सामग्री का निरीक्षण किए बिना और हर दूसरे बॉक्स की सामग्री से तुलना किए बिना अपनी दीवार बनाना खतरनाक है :

  • कुछ में मजबूत मैग्नेट होते हैं और अनुचित तरीके से संरेखित होने पर दीवार से बाहर अन्य चुंबकीय बक्से को धक्का देंगे।
  • कुछ बहुत गर्म या ठंडे होते हैं और आस-पास के स्थानों में रखे जाने पर बुरी तरह से प्रतिक्रिया करेंगे।

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

एक शुद्ध कार्यात्मक भाषा में, आपको यह देखने के लिए एक फ़ंक्शन के हस्ताक्षर की आवश्यकता है कि वह क्या करता है। बेशक, आप यह देखना चाहते हैं कि यह कितना अच्छा प्रदर्शन करता है, लेकिन आपको यह देखने की ज़रूरत नहीं है

एक अनिवार्य भाषा में, आप कभी नहीं जानते कि आश्चर्य अंदर क्या छिपा सकता है।


"शुद्ध कार्यात्मक भाषा में, आपको यह देखने के लिए एक फ़ंक्शन के हस्ताक्षर की आवश्यकता है कि यह क्या करता है।" - यह आम तौर पर सच नहीं है। हां, पैरामीट्रिक बहुरूपता की धारणा के तहत हम यह निष्कर्ष निकाल सकते हैं कि प्रकार का एक फ़ंक्शन (a, b) -> aकेवल fstफ़ंक्शन हो सकता है और यह कि प्रकार का एक फ़ंक्शन a -> aकेवल identityफ़ंक्शन हो सकता है , लेकिन आप (a, a) -> aउदाहरण के लिए, फ़ंक्शन के प्रकार के बारे में कुछ भी नहीं कह सकते हैं ।
जोर्ग डब्ल्यू मित्तग

2

जैसा कि मैं प्रतिस्थापन मॉडल (संदर्भात्मक पारदर्शिता (आरटी)) के साथ मोटे तौर पर समझता हूं, आप किसी फ़ंक्शन को उसके सरल भागों में डी-कंपोज कर सकते हैं। यदि अभिव्यक्ति आरटी है, तो आप अभिव्यक्ति को डी-कंपोज कर सकते हैं और हमेशा एक ही परिणाम प्राप्त कर सकते हैं।

हां, अंतर्ज्ञान काफी सही है। यहाँ अधिक सटीक होने के लिए कुछ संकेत दिए गए हैं:

जैसा आपने कहा, किसी भी आरटी अभिव्यक्ति का single"परिणाम" होना चाहिए । यही है, factorial(5)कार्यक्रम में एक अभिव्यक्ति दी गई है , इसे हमेशा एक ही "परिणाम" प्राप्त करना चाहिए। इसलिए, यदि एक निश्चित factorial(5)कार्यक्रम में है और इसकी पैदावार 120 है, तो इसे हमेशा 120 की पैदावार करनी चाहिए, जिसकी परवाह किए बिना "चरण क्रम" का विस्तार / गणना की जाती है - समय की परवाह किए बिना ।

उदाहरण: factorialकार्य।

def factorial(n):
    if n == 1:
        return 1
    return n * factorial(n - 1)

इस स्पष्टीकरण के साथ कुछ विचार हैं।

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

def first(y, z):
  return y

def second(x):
  return second(x)

first(2, second(3)) # result depends on eval. model

ऊपर दिए गए कोड में, firstऔर secondसंदर्भित रूप से पारदर्शी हैं, और फिर भी, अंत में अभिव्यक्ति अलग-अलग "परिणाम" देती है यदि सामान्य क्रम और आवेदन क्रम के तहत मूल्यांकन किया जाता है (बाद के तहत, अभिव्यक्ति रुक ​​नहीं जाती है)।

.... जो उद्धरणों में "परिणाम" का उपयोग करता है। चूंकि इसे रोकने के लिए अभिव्यक्ति की आवश्यकता नहीं है, इसलिए यह मान उत्पन्न नहीं कर सकता है। तो "परिणाम" का उपयोग धुंधला की तरह है। कोई कह सकता है कि एक RT अभिव्यक्ति हमेशा computationsएक मूल्यांकन मॉडल के तहत एक ही पैदावार देता है ।

तीसरा, foo(50)अलग-अलग स्थानों में कार्यक्रम में दो को अलग-अलग अभिव्यक्तियों के रूप में देखना आवश्यक हो सकता है - हर एक अपने स्वयं के परिणाम प्राप्त कर सकता है जो एक दूसरे से भिन्न हो सकते हैं। उदाहरण के लिए, यदि भाषा गतिशील गुंजाइश की अनुमति देती है, तो दोनों अभिव्यक्तियां, हालांकि शाब्दिक रूप से समान हैं, अलग-अलग हैं। पर्ल में:

sub foo {
    my $x = shift;
    return $x + $y; # y is dynamic scope var
}

sub a {
    local $y = 10;
    return &foo(50); # expanded to 60
}

sub b {
    local $y = 20;
    return &foo(50); # expanded to 70
}

डायनेमिक स्कोप गुमराह करता है क्योंकि यह आसान लगता xहै कि किसी के लिए यह एकमात्र इनपुट है foo, जब वास्तव में, यह है xऔर y। अंतर देखने का एक तरीका यह है कि प्रोग्राम को डायनेमिक स्कोप के बिना एक समतुल्य में बदल दिया जाए - यानी, स्पष्ट रूप से मापदंडों को पास करना, इसलिए परिभाषित करने के बजाय foo(x), हम कॉलर्स में स्पष्ट रूप से परिभाषित foo(x, y)और पास yकरते हैं।

मुद्दा यह है, हम हमेशा एक functionमानसिकता के अंतर्गत होते हैं : एक अभिव्यक्ति के लिए एक निश्चित इनपुट दिया जाता है, हमें एक "परिणाम" दिया जाता है। यदि हम एक ही इनपुट देते हैं, तो हमें हमेशा उसी "परिणाम" की अपेक्षा करनी चाहिए।

अब, निम्नलिखित कोड के बारे में क्या?

def foo():
   global y
   y = y + 1
   return y

y = 10
foo() # yields 11
foo() # yields 12

fooप्रक्रिया आर टी टूट जाता है क्योंकि वहाँ redefinitions हैं। यही है, हमने yएक बिंदु में परिभाषित किया, और बाद में उसी को फिर से परिभाषित किया y। उपरोक्त पर्ल उदाहरण में, yएस अलग-अलग बाइंडिंग हैं, हालांकि वे एक ही अक्षर का नाम "y" साझा करते हैं। यहाँ yवास्तव में वही हैं। इसलिए हम कहते हैं (पुनः) असाइनमेंट एक मेटा ऑपरेशन है: आप वास्तव में अपने प्रोग्राम की परिभाषा बदल रहे हैं।

मोटे तौर पर, लोग आमतौर पर अंतर को निम्नानुसार दर्शाते हैं: साइड-इफेक्ट फ्री सेटिंग में, आपके पास मैपिंग है input -> output। एक "अनिवार्य" सेटिंग में, आप input -> ouputएक के संदर्भ में stateउस समय के माध्यम से बदल सकते हैं।

अब, अपने संबंधित मूल्यों के लिए केवल भावों को प्रतिस्थापित करने के बजाय, stateप्रत्येक को प्रत्येक ऑपरेशन में परिवर्तनों को भी लागू करना पड़ता है जिसके लिए इसकी आवश्यकता होती है (और निश्चित रूप से, अभिव्यक्तियाँ stateगणना करने के लिए उसी से परामर्श कर सकती हैं )।

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


1
अच्छा लगा कि आपने संस्मरण का उल्लेख किया। इसका उपयोग आंतरिक राज्य के एक उदाहरण के रूप में किया जा सकता है जो बाहर दिखाई नहीं देता है: मेमोइज़ेशन का उपयोग करने वाला एक फ़ंक्शन अभी भी संदर्भात्मक रूप से पारदर्शी है, हालांकि आंतरिक रूप से यह राज्य और म्यूटेशन का उपयोग करता है।
जियोर्जियो
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.