वास्तविक पैटर्न वास्तव में सिर्फ डेटा एक्सेस की तुलना में काफी अधिक सामान्य है। यह एक डोमेन-विशिष्ट भाषा बनाने का एक हल्का तरीका है जो आपको एएसटी देता है, और फिर एएसटी को "निष्पादित" करने के लिए एक या एक से अधिक दुभाषियों का उपयोग करता है।
मुक्त मोनाड भाग एक एएसटी प्राप्त करने का एक आसान तरीका है जिसे आप बहुत सारे कस्टम कोड लिखने के बिना हास्केल के मानक मोनाड सुविधाओं (जैसे-नोटेशन) का उपयोग करके इकट्ठा कर सकते हैं। यह भी सुनिश्चित करता है कि आपका डीएसएल कंपोजिटेबल है : आप इसे भागों में परिभाषित कर सकते हैं और फिर भागों को एक संरचित तरीके से एक साथ रख सकते हैं, जिससे आप हास्केल के सामान्य अमूर्त कार्यों का लाभ उठा सकते हैं।
एक मुफ्त मोनाड का उपयोग करने से आपको एक कंपोज़ेबल डीएसएल की संरचना मिलती है ; आपको बस इतना करना है कि टुकड़ों को निर्दिष्ट करना है। आप बस एक डेटा टाइप लिखते हैं जो आपके डीएसएल में सभी कार्यों को शामिल करता है। ये क्रियाएं कुछ भी कर सकती हैं, न कि केवल डेटा एक्सेस। हालाँकि, यदि आपने अपने सभी डेटा एक्सेस को कार्रवाई के रूप में निर्दिष्ट किया है, तो आपको एक एएसटी मिलेगा जो डेटा स्टोर पर सभी प्रश्नों और कमांड को निर्दिष्ट करता है। आप इसे फिर भी पसंद कर सकते हैं, लेकिन इसे लाइव डेटाबेस के खिलाफ चलाएं, इसे मॉक के खिलाफ चलाएं, बस डिबगिंग के लिए कमांड को लॉग इन करें या प्रश्नों को अनुकूलित करने का प्रयास करें।
कहते हैं, एक प्रमुख मूल्य की दुकान के लिए एक बहुत ही सरल उदाहरण देखें। अभी के लिए, हम कुंजी और मान दोनों को स्ट्रिंग्स के रूप में मानेंगे, लेकिन आप थोड़े प्रयास के साथ प्रकार जोड़ सकते हैं।
data DSL next = Get String (String -> next)
| Set String String next
| End
nextपैरामीटर हमें कार्यों गठबंधन की सुविधा देता है। हम इसका उपयोग एक प्रोग्राम लिखने के लिए कर सकते हैं जो "फू" हो जाता है और उस मान के साथ "बार" सेट करता है:
p1 = Get "foo" $ \ foo -> Set "bar" foo End
दुर्भाग्य से, यह एक सार्थक डीएसएल के लिए पर्याप्त नहीं है। चूंकि हमने nextरचना के लिए उपयोग किया था , p1हमारे प्रोग्राम के समान लंबाई (यानी 3 कमांड) है:
p1 :: DSL (DSL (DSL next))
इस विशेष उदाहरण में, nextइस तरह का उपयोग करना थोड़ा अजीब लगता है, लेकिन यह महत्वपूर्ण है अगर हम चाहते हैं कि हमारे कार्यों में भिन्न प्रकार के चर हों। हम उदाहरण के लिए एक टाइप getऔर चाहते हैं set।
ध्यान दें कि nextप्रत्येक क्रिया के लिए फ़ील्ड अलग कैसे है। यह संकेत देता है कि हम इसका उपयोग DSLफ़न बनाने वाले के लिए कर सकते हैं :
instance Functor DSL where
fmap f (Get name k) = Get name (f . k)
fmap f (Set name value next) = Set name value (f next)
fmap f End = End
वास्तव में, यह एक फ़नकार बनाने का एकमात्र वैध तरीका है, इसलिए हम एक्सटेंशन derivingको सक्षम करके स्वचालित रूप से उदाहरण बनाने के लिए उपयोग कर सकते हैं DeriveFunctor।
अगला चरण Freeस्वयं प्रकार है। यही कारण है कि हम अपने एएसटी संरचना का प्रतिनिधित्व करने के लिए उपयोग करते हैं , DSLप्रकार के शीर्ष पर निर्माण करते हैं । आप इसे एक सूची की तरह प्रकार के स्तर पर सोच सकते हैं , जहां "विपक्ष" सिर्फ एक फ़नकार की तरह घोंसला बना रहा है DSL:
-- compare the two types:
data Free f a = Free (f (Free f a)) | Return a
data List a = Cons a (List a) | Nil
इसलिए हम Free DSL nextविभिन्न आकारों के कार्यक्रमों को एक ही प्रकार देने के लिए उपयोग कर सकते हैं :
p2 = Free (Get "foo" $ \ foo -> Free (Set "bar" foo (Free End)))
जिसके पास बहुत अच्छा प्रकार है:
p2 :: Free DSL a
हालांकि, इसके सभी निर्माणकर्ताओं के साथ वास्तविक अभिव्यक्ति अभी भी उपयोग करने के लिए बहुत अजीब है! यह वह जगह है जहाँ मोनाड भाग आता है। जैसा कि नाम "मुक्त मोनाद" का अर्थ है, Freeएक मोनाद है - जब तक f(इस मामले में DSL) एक फ़नकार है:
instance Functor f => Monad (Free f) where
return = Return
Free a >>= f = Free (fmap (>>= f) a)
Return a >>= f = f a
अब हम कहीं जा रहे हैं: हम doअपने डीएसएल एक्सप्रेशन को अच्छा बनाने के लिए नोटेशन का उपयोग कर सकते हैं । एकमात्र सवाल यह है कि इसमें क्या रखा जाए next? ठीक है, विचार Freeसंरचना के लिए संरचना का उपयोग करना है , इसलिए हम बस Returnप्रत्येक अगले क्षेत्र के लिए डालेंगे और सभी प्लंबिंग को डू-नोटेशन करते हैं:
p3 = do foo <- Free (Get "foo" Return)
Free (Set "bar" foo (Return ()))
Free End
यह बेहतर है, लेकिन यह अभी भी थोड़ा अजीब है। हमारे पास Freeऔर Returnसभी जगह हैं। खुशी से, एक पैटर्न है जिसका हम शोषण कर सकते हैं: जिस तरह से हम एक डीएसएल कार्रवाई को "लिफ्ट" करते हैं Freeवह हमेशा एक ही होता है - हम इसे लपेटते हैं Freeऔर इसके Returnलिए आवेदन करते हैं next:
liftFree :: Functor f => f a -> Free f a
liftFree action = Free (fmap Return action)
अब, इसका उपयोग करते हुए, हम अपने प्रत्येक कमांड के अच्छे संस्करण लिख सकते हैं और एक पूर्ण डीएसएल है:
get key = liftFree (Get key id)
set key value = liftFree (Set key value ())
end = liftFree End
इसका उपयोग करते हुए, यहां बताया गया है कि हम अपना कार्यक्रम कैसे लिख सकते हैं:
p4 :: Free DSL a
p4 = do foo <- get "foo"
set "bar" foo
end
साफ-सुथरी चाल यह है कि जबकि p4यह थोड़ा जरूरी कार्यक्रम की तरह दिखता है, यह वास्तव में एक अभिव्यक्ति है जिसका मूल्य है
Free (Get "foo" $ \ foo -> Free (Set "bar" foo (Free End)))
तो, पैटर्न के मुक्त मोनाड भाग ने हमें एक डीएसएल प्राप्त किया है जो अच्छे सिंटैक्स के साथ सिंटैक्स पेड़ों का उत्पादन करता है। हम उपयोग न करके भी कंपोजिट सब-ट्रीज़ लिख सकते हैं End; उदाहरण के लिए, हमारे पास followएक कुंजी हो सकती है , उसका मूल्य मिलता है और फिर वह कुंजी के रूप में उपयोग करता है:
follow :: String -> Free DSL String
follow key = do key' <- get key
get key'
अब followहमारे कार्यक्रमों में इस्तेमाल किया जा सकता है जैसे getया set:
p5 = do foo <- follow "foo"
set "bar" foo
end
तो हम अपने DSL के लिए कुछ अच्छी रचना और अमूर्तता प्राप्त करते हैं।
अब जब हमारे पास एक पेड़ है, तो हम पैटर्न के दूसरे भाग तक पहुंचते हैं: दुभाषिया। हम पेड़ की व्याख्या कर सकते हैं, हालांकि हम उस पर पैटर्न-मिलान करके पसंद करते हैं। यह हमें वास्तविक डेटा स्टोर के IOसाथ-साथ अन्य चीजों के खिलाफ कोड लिखने देगा । यहाँ एक काल्पनिक डेटा स्टोर के खिलाफ एक उदाहरण दिया गया है:
runIO :: Free DSL a -> IO ()
runIO (Free (Get key k)) =
do res <- getKey key
runIO $ k res
runIO (Free (Set key value next)) =
do setKey key value
runIO next
runIO (Free End) = close
runIO (Return _) = return ()
यह खुशी से किसी भी DSLटुकड़े का मूल्यांकन करेगा , यहां तक कि एक के साथ समाप्त नहीं हुआ है end। खुशी से, हम फ़ंक्शन का "सुरक्षित" संस्करण बना सकते हैं जो केवल endइनपुट प्रकार के हस्ताक्षर को सेट करके बंद किए गए कार्यक्रमों को स्वीकार करता है (forall a. Free DSL a) -> IO ()। पुराने हस्ताक्षर एक को स्वीकार करता है, जबकि Free DSL aके लिए किसी भी a (जैसे Free DSL String, Free DSL Intऔर इसी तरह), इस संस्करण केवल एक को स्वीकार करता है Free DSL aकि के लिए काम करता हर संभव a-which हम केवल के साथ बना सकते हैं end। यह गारंटी देता है कि जब हम काम पूरा करेंगे तो हम कनेक्शन बंद करना नहीं भूलेंगे।
safeRunIO :: (forall a. Free DSL a) -> IO ()
safeRunIO = runIO
(हम केवल runIOइस प्रकार को देकर शुरू नहीं कर सकते क्योंकि यह हमारे पुनरावर्ती कॉल के लिए ठीक से काम नहीं करेगा। हालांकि, हम फ़ंक्शन के दोनों संस्करणों को उजागर किए बिना runIOएक whereब्लॉक में परिभाषा को स्थानांतरित कर सकते हैं safeRunIOऔर समान प्रभाव प्राप्त कर सकते हैं।)
हमारे कोड को चलाना IOकेवल वही काम नहीं है जो हम कर सकते हैं। परीक्षण के लिए, हम इसे शुद्ध के State Mapबजाय चलाना चाह सकते हैं । उस कोड को लिखना एक अच्छा व्यायाम है।
तो यह मुफ्त मोनाड + दुभाषिया पैटर्न है। हम एक डीएसएल बनाते हैं, जो सभी प्लंबिंग करने के लिए मुफ्त मोनाड संरचना का लाभ उठाते हैं। हम अपने डीएसएल के साथ डू-नोटेशन और मानक मोनड कार्यों का उपयोग कर सकते हैं। फिर, वास्तव में इसका उपयोग करने के लिए, हमें किसी तरह इसकी व्याख्या करनी होगी; चूंकि पेड़ अंततः केवल एक डेटा संरचना है, हम इसकी व्याख्या कर सकते हैं, हालांकि हम विभिन्न उद्देश्यों के लिए पसंद करते हैं।
जब हम बाहरी डेटा स्टोर तक पहुंच का प्रबंधन करने के लिए इसका उपयोग करते हैं, तो यह वास्तव में रिपॉजिटरी पैटर्न के समान है। यह हमारे डेटा स्टोर और हमारे कोड के बीच में अंतर करता है, दोनों को अलग करता है। कुछ मायनों में, हालांकि, यह अधिक विशिष्ट है: "रिपॉजिटरी" हमेशा एक स्पष्ट एएसटी के साथ एक डीएसएल होता है जिसे हम तब उपयोग कर सकते हैं जो हमें पसंद है।
हालांकि, पैटर्न खुद ही इससे अधिक सामान्य है। इसका उपयोग बहुत सारी चीजों के लिए किया जा सकता है जो जरूरी नहीं कि बाहरी डेटाबेस या भंडारण में शामिल हों। यह समझ में आता है कि आप जहां भी चाहते हैं प्रभाव को ठीक करते हैं या एक डीएसएल के लिए कई लक्ष्य बनाते हैं।