कैटमॉर्फिज्म और पैरामोर्फिज्म के लिए एक ट्यूटोरियल
पुनरावर्तन योजनाएँ पुनरावृत्ति को दूर करने का एक तरीका है। कुछ ने तर्क दिया है पुनरावर्तन योजनाओं के बिना कार्यात्मक प्रोग्रामिंग अनिवार्य प्रोग्रामिंग के बराबर है for
लूप्स, बल्कि साथ goto
बयान।
उपयोग के रूप में
while
तथाfor
बजाय लूप्सgoto
अनिवार्य नियंत्रण प्रवाह के लिए संरचना और सद्भाव लाता है, हस्तलिखित रिकर्सन पर रिकर्सन योजनाओं का उपयोग रिकर्सिव कंप्यूटेशंस के समान संरचना लाता है। यह अंतर्दृष्टि इतनी महत्वपूर्ण है कि मैं इसे दोहराऊंगा: पुनरावर्तन योजनाएँ मुहावरेदार कार्यात्मक प्रोग्रामिंग के लिए उतनी ही आवश्यक हैं जितनी किfor
तथाwhile
मुहावरेदार अनिवार्य प्रोग्रामिंग हैं। — पैट्रिक थॉम्पसन
मेरी इतनी मजबूत राय नहीं है। मुझे लगता है कि पुनरावर्तन योजनाएँ मन उड़ाने वाली दिलचस्प हैं, और मैं इससे पक्षपाती भी हूँ पैट्रिक बह्र ने ट्री ऑटोमेटा के साथ जो संबंध बनाया चूँकि मैंने ट्री ऑटोमेटा के साथ काफी काम किया है, और इसे व्यवहार में लागू होते देखना आपके लिए जितना रोमांचक होगा, उससे कहीं अधिक मेरे लिए रोमांचक है।
हमारे द्वारा उपयोग किए जाने वाले पुनरावर्तन के विपरीत, ये पुनरावर्तन योजनाएँ हमें उस प्रकार के पुनरावर्तन के बारे में बहुत विशिष्ट होने से रोकती हैं जिसे हम अपने कार्य में लागू करेंगे। पुनरावर्तन योजनाएँ पठनीयता (अंततः) में सहायता करती हैं और पुनरावर्ती मूल्यांकन कार्यों के सरल कार्यान्वयन की अनुमति देती हैं, लेकिन आपके डेटा प्रकारों को परिभाषित करते समय कुछ अतिरिक्त बॉयलरप्लेट की आवश्यकता होती है और उनका उपयोग करने के तरीके सीखने के लिए थोड़े अभ्यास की आवश्यकता होती है।
रिकर्सन कई प्रकार के होते हैं, इसलिए उन सभी को कवर करने के लिए हमारे पास कुछ योजनाएँ हैं। इस लेख में, मैं दो सबसे आम पुनरावर्तन योजनाओं की व्याख्या करने की कोशिश करूँगा:
- कैटामॉर्फिज्म: एक साधारण बॉटम-अप रिकर्सन के लिए एक फैंसी नाम।
- पैरामॉर्फिज्म: थोड़ा स्मार्ट बॉटम-अप रिकर्सन के लिए एक और फैंसी नाम जिसमें कुछ स्टैक जानकारी की आवश्यकता होती है। अभी भी काफी सरल है।
टिप्पणी: इस लेख के लिए हास्केल की समझ की आवश्यकता है, कम से कम एक Functor को लागू करने तक।
पुनरावर्तन योजनाओं की व्याख्या करने के लिए एक उदाहरण की आवश्यकता होती है। इसके लिए, मैंने रेगुलर एक्सप्रेशंस के लिए डेरिवेटिव एल्गोरिथम चुना है, जिसे मैंने कवर किया है एक पिछला लेख.
इस पोस्ट के लिए, मैं सामान्य पुनरावर्तन का उपयोग करके केवल कार्यान्वयन कोड शामिल करूंगा ताकि हम देख सकें कि पुनरावर्तन योजनाओं को जोड़ने से कैसे फर्क पड़ेगा। यहाँ पूर्ण रेगुलर एक्सप्रेशन मिलान एल्गोरिथम है जो हमारे द्वारा उपयोग किए जाने वाले पुनरावर्तन का उपयोग करता है।
व्युत्पन्न या deriv
इनपुट के रूप में एक वर्ण दिए जाने पर, फ़ंक्शन मिलान के लिए छोड़ी गई नियमित अभिव्यक्ति लौटाता है। हम इसे इनपुट स्ट्रिंग का उपयोग करके लूप करते हैं foldl
, नियमित अभिव्यक्ति प्राप्त करने के लिए जो पूरी स्ट्रिंग का उपभोग करने के बाद मिलान करने के लिए छोड़ दिया गया है। फिर हम जाँचते हैं कि परिणामी रेगुलर एक्सप्रेशन का उपयोग करके खाली स्ट्रिंग से मेल खाता है या नहीं nullable
फ़ंक्शन, यह जानने के लिए कि क्या रेगेक्स जिसने पूरे स्ट्रिंग का उपभोग किया है, उस स्ट्रिंग से मेल खाता है।
तकनीकी रूप से, foldl
पहले से ही कुछ पुनरावर्तन को अमूर्त कर दिया गया है, लेकिन हम पुनरावर्तन योजनाओं के साथ इस अमूर्तता को दूसरे स्तर पर ले जा रहे हैं।
इससे पहले कि हम बहुत गहराई में जाएँ, आइए देखें कि हम कहाँ जा रहे हैं। nullable
फ़ंक्शन एक पूर्ण कैटमोर्फिज्म है क्योंकि यह केवल नीचे-ऊपर रिकर्सन करता है। बॉटम-अप रिकर्सन तब होता है जब किसी फ़ंक्शन का परिणाम केवल उसके बच्चों पर लागू किए गए पुनरावर्ती फ़ंक्शन के परिणामों पर निर्भर करता है, न कि पुनरावर्ती फ़ंक्शन में दिए गए किसी मध्यवर्ती परिणाम पर। हम अपनी जगह लेने जा रहे हैं nullable
निम्नलिखित समतुल्य फ़ंक्शन के साथ उपरोक्त मूल कार्यान्वयन में कार्य करें:
ध्यान दें कि कैसे nullableAlg
फ़ंक्शन को स्वयं को कोई पुनरावर्ती कॉल करने की आवश्यकता नहीं है। इस लेख में, हम बताएंगे कि यह कैसे संभव है और साथ ही:
- क्या एक
FAlgebra
है - एक नया प्रकार क्यों कहा जाता है
RegexF
- कैसे
cata
समारोह नीचे-ऊपर रिकर्सन करता है
हमारी पुनरावर्तन योजनाओं को हमारे पुनरावर्तन के दौरान मध्यवर्ती परिणामों को संग्रहीत करने के लिए स्थान की आवश्यकता होती है। हमें इन परिणामों के लिए एक कंटेनर चाहिए। हम जानते हैं कि फ़नकार एक बेहतरीन कंटेनर है, इसलिए हम अपना टर्न करेंगे Regex
पैरामीट्रिजिंग करके डेटा टाइप को फ़ंक्टर में बदलें। यह पैरामीटर हमारे पुनरावर्ती एल्गोरिदम के दौरान विभिन्न मध्यवर्ती परिणामों को संग्रहीत कर सकता है।
हम नाम बदलकर शुरू करते हैं Regex
प्रति RegexF
जहां F
functor के लिए खड़ा है। हम सभी रिकर्सिव को पैरामीट्रिज करते हैं Regex
खेत। यहाँ कोड है:
यदि आप वापस जाते हैं और की मूल परिभाषा को देखते हैं Regex
आप देखेंगे कि हमने की सभी पुनरावर्ती घटनाओं को बदल दिया है Regex
साथ r
. हमने पैरामीटर का नाम दिया r
पुनरावर्ती या परिणाम के लिए। इस r
बूलियन परिणामों को संग्रहीत करने के लिए उपयोग किया जाएगा क्योंकि हम कैटामोर्फिज्म का उपयोग करके अपने अशक्त कार्य के लिए बॉटम-अप रिकर्सन करते हैं, लेकिन जल्द ही उस पर और अधिक।
सबसे पहले, हमें समस्या है। हमें यह चुनना होगा कि कौन सा पैरामीटर सेट करना है r
करने के लिए, हमारे मूल प्राप्त करने के लिए Regex
वापस की परिभाषा का उपयोग कर RegexF
.
- अगर हम बनाते हैं
r
एकBool
हम केवल बहुत ही छोटे व्यंजक बना सकते हैं जैसेEmptyString
लेकिन अन्य इससे भी कम अर्थ निकालते हैं:Concat True False
. - अगर हम चुनते हैं
r
होनाRegex
हम पाते हैंRegexF Regex
जो काम करेगा, लेकिन हम अपनी रिकर्सिव फ़ैक्टर संपत्ति खो देते हैं। - हम चुनने की कोशिश कर सकते हैं
r
होनाRegexF
तो हमें मिलता हैRegexF RegexF
यह करीब है, लेकिन अब हमें दूसरे की आवश्यकता हैRegexF
दूसरे के लिए पैरामीटर होनाRegexF
तो हम प्राप्त करते हैंRegexF (RegexF RegexF)
, लेकिन यह कभी न खत्म होने वाली समस्या है। किसी बिंदु पर, हम चाहते हैं कि यह पुनरावर्तन समाप्त हो। - हम दो स्तरों की कोशिश कर सकते हैं
RegexF
तो हमें मिलता हैRegexF (RegexF (RegexF ()))
लेकिन तब हम केवल दो स्तरों की गहराई के नियमित भावों का प्रतिनिधित्व कर सकते हैं।
हम एक मज़ेदार चाहते हैं, लेकिन हम अधिकतम रिकर्सन गहराई तक सीमित नहीं होना चाहते हैं।
एक निश्चित बिंदु वह होता है जहां एक फ़ंक्शन अभिसरण करता है या जहां फ़ंक्शन का इनपुट आउटपुट के बराबर होता है। उदाहरण के लिए: f(x) = x² का एक निश्चित बिंदु 1 है क्योंकि 1² = 1 है।
हम अपने समकक्ष को फिर से बना सकते हैं Regex
डेटा प्रकार एक निश्चित बिंदु का उपयोग कर:
type Regex = Fix RegexF
यह कैसा है Fix RegexF
हमारे मूल के बराबर Regex
?
इसे समझने के लिए, हमें बारीकी से देखने की जरूरत है Fix
. Fix
एक निश्चित बिंदु डेटा प्रकार है:
newtype Fix f = Fix (f (Fix f))
मुझे इसके चारों ओर अपना सिर लपेटने में काफी समय लगा, लेकिन आखिरकार, मेरा दिमाग एक निश्चित बिंदु पर पहुंच गया।
लेकिन अगर आप प्रत्येक टुकड़े पर एक शांत नज़र डालें, तो यह समझ में आ सकता है:
newtype Fix f = Fix (f (Fix f))
- सबसे पहला
Fix
प्रकार का नाम है। - दूसरा
Fix
प्रकार निर्माता नाम है। - तीसरा
Fix
प्रकार का प्रयोग किया जा रहा है।
Fix
प्रकार एक प्रकार का पैरामीटर लेता है जिसे कहा जाता है f
. f
एक मज़ेदार है, जिसका अर्थ है कि यह एक प्रकार का पैरामीटर भी लेता है। इस स्थिति में, प्रकार पैरामीटर अंतिम होगा Fix f
.
अगर हम चुनते हैं RegexF
जैसा हमारा f
हमारा प्रकार पैरामीटर r
होगा Fix RegexF
लेकिन हम यह जानते हैं Fix RegexF
सच है Fix (RegexF (Fix RegexF))
जो हम जानते हैं वास्तव में है Fix (RegexF (Fix (RegexF (Fix RegexF))))
, आदि। यह वही है जो हम चाहते थे। अब हम किसी भी गहराई के नियमित भावों का प्रतिनिधित्व कर सकते हैं। फेलिक्स ने इसे ठीक किया!
रेक इट रैल्फ हमारे को अनफिक्स भी कर सकता है Fix
डेटा प्रकार, जो कैटमॉर्फिज़्म फ़ंक्शन के कार्यान्वयन में उपयोगी होगा।
wreckit
एक पैटर्न से मेल खाता है Fix
और अंदर मान लौटाता है:
wreckit :: Fix f -> f (Fix f)
wreckit (Fix x) = x
अब जबकि हमने अपना फंक्टर एक्सप्रेशन बना लिया है, आइए कुछ सिद्धांत को कवर करें कि हम रिकर्सन को कैसे अमूर्त करेंगे।
एक बीजगणित में निम्न शामिल हैं:
- भाव बनाने की क्षमता। उदाहरण के लिए, द
Regex
कंस्ट्रक्टर और - उदाहरण के लिए, इन भावों का मूल्यांकन करने की क्षमता
nullable :: Regex -> Bool
एक बीजगणित के दो भागों से मिलकर होने के बावजूद, मूल्यांकन कार्य को आमतौर पर बीजगणित कहा जाता है:
type Algebra e r = e -> r
इसका मतलब है nullable
कार्य प्रभावी रूप से है NullableAlgebra
type NullableAlgebra = Algebra Regex Bool
nullable :: NullableAlgebra
श्रेणी सिद्धांत में एक एफ-बीजगणित में निम्नलिखित शामिल हैं:
- उदाहरण के लिए, फ़ंक्टर वाले भावों को बनाने की क्षमता
RegexF r
तथा - इन अभिव्यक्तियों का मूल्यांकन करने की क्षमता, उदाहरण के लिए:
nullable :: RegexF Bool -> Bool
.
इसका मतलब है कि एफ-बीजगणित का प्रकार है:
type FAlgebra f r = f r -> r
nullableAlg
समारोह है NullableAlgebra
:
type NullableFAlgebra = FAlgebra RegexF Bool
nullableAlg :: NullableFAlgebra
कैसे करता है cata
फ़ंक्शन सार नीचे-ऊपर रिकर्सन को दूर करता है? यहाँ पूरा समारोह है:
cata :: Functor f => FAlgebra f r -> Fix f -> r
cata alg = alg . fmap (cata alg) . wreckit
यह बहुत सारगर्भित है। आइए इसे और अधिक विशिष्ट बनाएं। पठनीयता में सहायता के लिए, पहले आइए एल्म के पाइप ऑपरेटर को हास्केल में जोड़ेंजो के बराबर है &
हास्केल में ऑपरेटर, लेकिन यह यूनिक्स पाइप की तरह अधिक दिखता है और दिशा को इंगित करता है। क्षमा करें, मुझे यह पढ़ना आसान लगता है।
infixl 0 |>
(|>) :: a -> (a -> b) -> b
x |> f = f x
अब, चलिए बनाते हैं cata
कार्य कार्यान्वयन के आवेदन के लिए अधिक विशिष्ट है nullableAlg
का कार्य:
cata :: NullableFAlgebra -> Regex -> Bool
cata nullableAlg regex =
wreckit regex
|> fmap (cata nullableAlg)
|> nullableAlg
पहले कदम के रूप में, wreckit
हमारा ले जाएगा Regex
और बाहरी को हटा दें Fix
:
wreckit :: Regex -> RegexF Regex
अब हमारे पास एक फ़ंक्टर है, जिसका अर्थ है कि हम कर सकते हैं fmap
ऊपर RegexF
. fmap
एक स्तर नीचे की ओर लौटता है। जिस फ़ंक्शन के साथ हम दोबारा काम करना चाहते हैं वह है nullable
समारोह जो के बराबर है cata nullableAlg
.
nullable :: Regex -> Bool
nullable = cata nullableAlg
रिकर्सन के प्रत्येक स्तर पर, हम अपने को कॉल करना चाहते हैं nullable
समारोह। तो हर बार हम fmap
एक स्तर नीचे, हम नीचे गुजरते हैं cata nullableAlg
निचले स्तर पर बुलाया जाएगा।
fmap (cata nullableAlg) :: RegexF Regex -> RegexF Bool
जैसा कि हम बैक अप की पुनरावृत्ति करते हैं, हम वापस लौटते हैं RegexF Bool
जो के मध्यवर्ती परिणामों को संग्रहीत करता है nullable
निचले स्तरों पर गणना। फिर हमें इसे फाइनल से गुजरना होगा nullableAlg
फाइनल पाने के लिए Bool
नतीजा।
nullableAlg :: RegexF Bool -> Bool
इसका मतलब है कि अब हम परिभाषित कर सकते हैं nullableAlg
बिना किसी रिकर्सन के काम करता है क्योंकि cata
function हमारे लिए सभी बॉटम-अप रिकर्सन करेगा nullable
समारोह:
यह एक कैटमॉर्फिज्म का उपयोग करने का सिर्फ एक उदाहरण था, और कैटमोर्फिज्म सरल बॉटम-अप रिकर्सन तक सीमित है, तो चलिए एक और रिकर्सन स्कीम के बारे में सीखते हैं।
इससे पहले कि हम अपनी अगली पुनरावर्तन योजना देखें, आप इस बारे में चिंतित हो सकते हैं कि एपीआई कैसा दिखेगा। आप उपयोगकर्ताओं को कैसे समझाएंगे कि आपके पुस्तकालय के उपयोगकर्ताओं के लिए एक निश्चित बिंदु क्या है? विचार यह है कि हम इन कार्यान्वयन विवरणों को पुस्तकालय के आंतरिक भाग तक सीमित रखेंगे। हमारा एपीआई किसी भी निश्चित बिंदु को उजागर नहीं करेगा। यह उजागर नहीं करेगा nullableAlg
इस पुस्तकालय के उपयोगकर्ताओं के लिए कार्य करता है, केवल nullable
समारोह। वही डेटा टाइप कंस्ट्रक्टर्स के लिए जाता है। हम एक्सपोज नहीं करना चाहते Fix
हमारे पुस्तकालय के बाहर, इसलिए हम स्मार्ट कंस्ट्रक्टर बनाते हैं जो उपयोगकर्ता के लिए निश्चित बिंदुओं का निर्माण करते हैं जिन्हें हम उजागर कर सकते हैं:
लागू करते समय यह हमारी अपनी आंतरिक सुविधा के लिए भी होगा deriv
अगले भाग में पैरामोर्फिज्म का उपयोग करते हुए कार्य करें।
यदि आप लूप के लिए नहीं समझते हैं, तो वे एक प्राकृतिक संख्या पर एक पैरामोर्फिज्म हैं — जोसेफ स्वेनिंग्सन
आइए मूल को देखें deriv
कार्य फिर से:
deriv
फ़ंक्शन एक साधारण बॉटम-अप रिकर्सन नहीं है, क्योंकि Concat
चरण को जाँचने की आवश्यकता है कि क्या पहली अभिव्यक्ति है r
अशक्त है। यह वह जानकारी है जो केवल स्टैक पर उपलब्ध है। एक पैरामॉर्फिज्म न केवल फंक्शनल पैरामीटर में एक आंतरायिक परिणाम रखता है, बल्कि मूल अभिव्यक्ति की एक प्रति भी रखता है ताकि हम यह जांच सकें कि यह अशक्त है या नहीं। इसके लिए एक नए बीजगणित की आवश्यकता होती है जिसे a कहा जाता है RAlgebra
:
type RAlgebra f r = f (Fix f, r) -> r
फ़ंक्टर में अब एक टपल होता है, जहाँ पहला पैरामीटर मूल अभिव्यक्ति की एक प्रति है और दूसरा मध्यवर्ती परिणाम है। हमारी DeriveRAlgebra
थोड़ा भ्रमित करने वाला लगेगा क्योंकि हमारा इंटरमीडिएट परिणाम मूल की प्रति के समान प्रकार का है:
type DeriveRAlgebra = RAlgebra RegexF Regex :: RegexF (Regex, Regex) -> Regex
इस RAlgebra
द्वारा मूल्यांकन किया जाएगा para
समारोह:
para :: (Functor f) => RAlgebra f r -> Fix f -> r
para alg f =
wreckit f
|> fmap (\x -> (x, para alg x))
|> alg
हम इसे और अधिक विशिष्ट बना सकते हैं derivAlg
समारोह:
para :: DeriveRAlgebra -> Regex -> Regex
para derivAlg regex =
wreckit regex
|> fmap (\x -> (x, para derivAlg x))
|> derivAlg
से फर्क सिर्फ इतना है cata
कार्य मूल की प्रति का भंडारण है Regex
, x
टपल में, न केवल मध्यवर्ती परिणाम, para derivAlg x
.
- हम इसकी बाहरी परत को हटा देते हैं
Fix
का उपयोग करते हुएwreckit :: Regex -> RegexF Regex
. - हम उपयोग करने वाले मज़ेदार के एक स्तर को दोबारा शुरू करते हैं
fmap
. - निचले स्तरों पर हम जो कार्य लागू करते हैं वह एक लेता है
Regex
और मूल लौटाता हैRegex
साथ ही व्युत्पन्नRegex
:Regex -> (Regex, Regex)
- परिणाम एक functor में है,
RegexF (Regex, Regex)
जिसका उपयोग करके हम मूल्यांकन कर सकते हैं:derivAlg c :: RegexF (Regex, Regex) -> Regex
हमारे अंतिम व्युत्पन्न प्राप्त करने के लिएRegex
.
इसका मतलब है कि अब हम परिभाषित कर सकते हैं derivAlg
बिना किसी पुनरावर्तन के कार्य करता है, क्योंकि para
function हमारे लिए सभी रिकर्सन कार्य करेगा deriv
समारोह:
आप देख सकते हैं कि हमें इनपुट पैरामीटर के चारों ओर स्वैप करने की आवश्यकता है derivAlg
इसे काम करने के लिए कार्य करें।
ये पुनरावर्तन योजनाओं का उपयोग करने के केवल दो उदाहरण थे, लेकिन यह हमारे एल्गोरिथम को पूरा करने के लिए पर्याप्त है:
आप संपूर्ण पा सकते हैं GitHub पर डेमो.
हमने केवल दो पुनरावर्ती योजनाओं को कवर किया है, लेकिन कई अन्य हैं:
- के आलावा
para
तथाcata
एक और तह कहा जाता हैhisto
. हिस्टोमॉर्फिज्म सभी पुनरावर्ती गणनाओं के इतिहास को संरक्षित करता है, जो एल्गोरिदम के लिए उपयोगी है, जिसके लिए फिबोनाची की तरह दक्षता के लिए मेमोइज़ेशन की आवश्यकता होती है। - अनफोल्ड, शामिल करें
ana
,apo
तथाfutu
. एनामॉर्फिज्म कैटमॉरपिज्म की श्रेणी थ्योरी डुअल है। यूनानी में,cata
विनाश का मतलब है, जबकिana
मतलब बिल्डिंग। एक एनामॉर्फिज्म पुनरावर्ती रूप से एक अभिव्यक्ति का निर्माण कर सकता है, उदाहरण के लिए दी गई लंबाई के साथ शून्य की सूची बनाना। - फिर रिफॉल्ड्स भी हैं:
hylo
जो कि हैcata
एक के बादana
तथाchrono
जो कि हैhisto
और एfutu
.
यदि आप पुनरावर्तन योजनाओं के बारे में अधिक जानने में रुचि रखते हैं तो मैंने संदर्भ अनुभाग में कुछ संसाधनों को जोड़ा है ताकि आप अपने स्वयं के महाकाव्य रोमांच को मॉर्फ के साथ जारी रख सकें:
- एंडोर पेन्ज़ेस अन्य सभी पुनरावर्तन योजनाओं को प्रूफरीडिंग और समझाने के लिए, खासकर जब से मैं अभी भी कुछ ही समझता हूं।
- मैक्स हाइबर गलती खोजने और बॉटम-अप रिकर्सन की बेहतर व्याख्या का सुझाव देने के लिए।