3 पॉइंट द्वारा GN⁺ 2025-01-20 | 2 टिप्पणियां | WhatsApp पर शेयर करें

Side effects को first-class values की तरह संभालना

  • Haskell में side effects (जैसे random number generation, output आदि) को first class value की तरह treat किया जाता है
  • यानी, randomRIO(1, 6) जैसे side effect पैदा करने वाले function call का return value खुद result नहीं होता, बल्कि वह एक ऐसा object लौटाता है जो “कभी चलाए जाने वाले action” को describe करता है
  • यह object वास्तव में execute होने पर random value बनाता है, लेकिन उससे पहले इसमें सिर्फ execution plan होता है
  • IO Int जैसा type उस “action” को दर्शाता है जो वास्तव में चलने पर Int पैदा करेगा; यह call करते ही execute नहीं होता, बल्कि बाद में ज़रूरत पड़ने पर execute होता है
  • इस विशेषता की वजह से पारंपरिक procedural languages के “function call = immediate execution” मॉडल से अलग, Haskell में side effects को compose किया जा सकता है और बाद में वास्तव में execute किया जा सकता है

do blocks को समझना

  • do block कोई जादुई syntax नहीं है; यह मूलतः दो operators से बना है जो side effects को bind करते हैं और sequence में execute करते हैं

then

  • *> operator पहले बाईं ओर वाले side effect को execute करता है, उसका result फेंक देता है, और फिर दाईं ओर वाले side effect को execute करता है
  • उदाहरण के लिए putStr "hello" *> putStrLn "world" दो outputs को sequence में जोड़कर एक IO () action बनाता है
  • do block में कई lines लिखने पर अंदर ही अंदर इसी तरह के sequential execution operator का उपयोग होता है

bind

  • >>= operator बाईं ओर के side effect को execute करके मिला value दाईं ओर के function को देता है
  • उदाहरण: randomRIO(1, 6) >>= print_side एक ऐसा side effect बनाता है जो dice result को print_side को देकर print करता है
  • do block में \<- pattern इसी operator को आसान रूप में व्यक्त करता है

Two operators are all of do blocks

  • अंततः do block इन्हीं दो operators *>, >>= से बना है
  • Code readability और convenience की वजह से do syntax बहुत इस्तेमाल होता है, लेकिन Haskell की ताकत का बेहतर उपयोग करने के लिए इससे भी अधिक समृद्ध side-effect composition functions का इस्तेमाल करना चाहिए

Side effects पर काम करने वाले functions

  • Standard library में side effects को और विविध तरीकों से संभालने के लिए कई functions मौजूद हैं

pure

  • pure x ऐसा action बनाता है जो “किसी अतिरिक्त side effect के बिना x value को result के रूप में पैदा करता है”
  • उदाहरण: loaded_die = pure 4 ऐसा IO Int बनाता है जो हमेशा 4 लौटाता है

fmap

  • fmap :: (a -> b) -> IO a -> IO b के रूप में, यह side effect के result value पर pure function apply करके नया result value बनाने वाला action तैयार करता है
  • उदाहरण: length <$> getEnv "HOME" की तरह, environment variable लाने वाले side effect पर length apply करके उसकी length निकालने वाला action बनाया जा सकता है

liftA2, liftA3, …

  • liftA2, liftA3 जैसे functions कई side-effect results को एक pure function के साथ combine करके नया side effect बनाते हैं
  • उदाहरण: liftA2 (+) (randomRIO(1,6)) (randomRIO(1,6)) ऐसा side effect बनाता है जो दो dice values का sum निकालता है
  • यही काम <>$ और <*> के combination से भी किया जा सकता है

Intermission: what’s the point?

  • यह तरीका दूसरी भाषाओं में संभव साधारण functionality जैसा लग सकता है, लेकिन Haskell में इसका लाभ यह है कि side-effect actions को कभी भी variables में निकालकर या दोबारा combine करके भी execution timing या result नहीं बदलता
  • Side effects को स्वतंत्र रूप से संभालने की वजह से code refactoring में भ्रम कम होता है, और equational reasoning के आधार पर सुरक्षित reuse संभव होता है

sequenceA

  • sequenceA [IO a] -> IO [a] “side-effect actions की list” को “list result देने वाले एक single side-effect action” में बदल देता है
  • उदाहरण: कई log actions को list में जमा करके, बाद में sequenceA से एक बार में execute किया जा सकता है
  • अनंत बार दोहराए जाने वाले side effects (जैसे repeat (randomRIO(1,6))) को भी list में रखकर, ज़रूरत के अनुसार सिर्फ take n करके sequenceA से execute किया जा सकता है

Interlude: convenience functions

  • void, sequenceA_, replicateM, replicateM_ आदि तब उपयोगी होते हैं जब result value का उपयोग नहीं करना हो या किसी action को बार-बार चलाना हो
  • उदाहरण: replicateM_ 500 (putStrLn "I will not cheat again.") की तरह repeat count को manually track किए बिना side effect को कई बार execute किया जा सकता है

traverse

  • traverse :: (a -> IO b) -> [a] -> IO [b] list के हर element पर side-effect function apply करके results को list में इकट्ठा करने वाला action बनाता है
  • sequenceA वास्तव में traverse id के बराबर है, और traverse_ वह version है जो results को discard कर देता है

for

  • for की functionality traverse जैसी ही है, लेकिन यह arguments को उल्टे क्रम में लेता है

  • उदाहरण: for numbers $ \n -> ... के रूप में “for loop” जैसी syntax को प्राकृतिक ढंग से व्यक्त किया जा सकता है

  • ऐसी composition की वजह से जिन चीज़ों के लिए दूसरी भाषाओं में अलग syntax चाहिए—जैसे repetition, traversal, data structure transformation—उन्हें Haskell में library functions के composition से लागू किया जा सकता है

Effects की first-class प्रकृति का पूरा उपयोग

  • Haskell में side effects को first-class values की तरह सक्रिय रूप से उपयोग करने पर code duplication कम किया जा सकता है और structure बेहतर बनाया जा सकता है
  • उदाहरण के लिए cache इस्तेमाल करने वाले large-number prime factorization logic में IO की जगह State आदि का उपयोग करके ऐसी संरचना बनाई जा सकती है जहाँ “effect मौजूद है लेकिन बाहर की दुनिया पर असर नहीं पड़ता”
  • इस तरह के structured effects सिर्फ ज़रूरी हिस्सों पर लागू होते हैं, जबकि बाकी code pure functions के रूप में बना रह सकता है; इससे safety और flexibility दोनों मिलती हैं
  • अंत में evalState आदि से वास्तविक effect को run करके result को pure value में बदला जा सकता है

वे चीज़ें जिनकी आपको कभी परवाह करने की ज़रूरत नहीं

  • पुराने Haskell दौर से चले आ रहे कई नाम (>>, return, mapM आदि) को आधुनिक functions (*>, pure, traverse आदि) से बदला जा सकता है
  • ये नाम “पुराने नामकरण” या monad-केंद्रित design से आए थे, जबकि आजकल Applicative या अधिक सामान्य Functor-आधारित approach की सिफारिश की जाती है

Appendix A: सफलता और निरर्थकता से बचना

  • “Haskell avoids success” का मतलब यह है कि “भाषा लोकप्रियता या convenience के लिए अपने मूल मूल्यों से समझौता नहीं करती”
  • “Haskell is useless” का संदर्भ यह है कि शुरुआत में यह सिर्फ पूरी तरह pure functions की अनुमति देने के कारण ऐसा लगता था जैसे भाषा में कुछ भी व्यावहारिक नहीं किया जा सकता; बाद में side effects को ‘first-class’ रूप में संभालने की विधि आने से इसमें practicality आई

Appendix B: Why fmap maps over both side effects and lists

  • fmap का रूप बहुत सामान्य है (Functor f => (a -> b) -> f a -> f b), इसलिए यह list, Maybe, IO जैसे अलग-अलग containers या effect types पर समान रूप से लागू होता है
  • List पर fmap लगाने से function हर element पर लागू होता है, और IO पर लगाने से function result value पर लागू होता है
  • इस तरह “ऐसी सभी structures जिन पर function apply किया जा सकता है” को Functor कहा जाता है

Appendix C: Foldable and Traversable

  • Foldable वह structure है जिसमें elements को traverse करते हुए process किया जा सकता है
  • Traversable ऐसा structure है जिसमें traverse करने के साथ-साथ उसी shape की structure को नए elements के साथ फिर से बनाया जा सकता है
  • sequenceA या traverse को values इकट्ठा करते समय मूल structure बनाए रखना हो तो वह structure Traversable होना चाहिए
  • Tree या Set जैसी data structures में structure values के अनुसार बदल सकता है, इसलिए ऐसे मामलों में सिर्फ traversal संभव होने (Foldable) और वास्तव में structure को पुनर्निर्मित कर पाने (Traversable) के बीच अंतर किया जाता है
  • ज़रूरत के अनुसार पहले list में बदलकर फिर traverse इस्तेमाल करने जैसे तरीकों से effects को लचीले ढंग से संभाला जा सकता है

2 टिप्पणियां

 
bbulbum 2025-01-21

Reddit देखते हुए ऐसे काफ़ी विज्ञापन दिख जाते हैं.. लेकिन नाम से ही थोड़ा मानसिक अवरोध सा महसूस होता है.
किसी तरह यह बहुत कठिन और शक्तिशाली भाषा जैसी लगती है..

 
GN⁺ 2025-01-20
Hacker News राय
  • Haskell का type system दूसरी लोकप्रिय भाषाओं की तुलना में जटिल है। खासकर *>, <*>, <* जैसे operators पूरे codebase में learning curve बढ़ा देते हैं

    • अगर एक महीने तक Haskell इस्तेमाल न करें, तो productivity बनाए रखने के लिए >>= और >> जैसे operators फिर से पढ़ने पड़ते हैं
    • लोगों से चर्चा किए बिना अकेले Haskell के concepts पढ़ना मुश्किल होता है
  • Haskell imperative programming को बेहतर बनाने में मदद करता है

    • first-class effects और patterns का इस्तेमाल करके boilerplate code हटाया जा सकता है
    • type safety की वजह से अपेक्षाकृत कम bugs वाला code जल्दी लिखा जा सकता है
  • traverse/mapM का generalized version सिर्फ lists ही नहीं बल्कि सभी Traversable types पर काम करता है, और यह बहुत उपयोगी है

    • इसे traverse :: Applicative f => (a -> f b) -> t a -> f (t b) के रूप में इस्तेमाल किया जा सकता है
    • दूसरी भाषाओं में ऐसा ही effect पाने के लिए बहुत सारा code हाथ से लिखना पड़ता था
  • Haskell में शक्तिशाली monads हैं, जो Haskell को और अधिक procedural बनाते हैं

    • do block में intermediate variables का इस्तेमाल किया जा सकता है
  • Haskell में लिखे गए software में ImplicitCAD शामिल है

  • Haskell का code procedural language की तरह पढ़ा जाता है, लेकिन side-effect functions के साथ काम करते समय फायदे देता है

    • IO monad के साथ काम करना जटिल है, और जब दूसरे monad types इस्तेमाल करने हों तो यह और भी जटिल हो जाता है
  • >> <i>> का पुराना नाम है, और दोनों operators left-associative हैं

    • >> को infixl 1 के रूप में define किया गया है और <i>> को infixl 4 के रूप में define किया गया है, इसलिए <i>> >> की तुलना में अधिक मजबूती से bind करता है
  • Haskell का IO a और a asynchronous और synchronous जैसा महसूस हो सकता है

    • पहला एक promise/future लौटाता है जिसका इंतज़ार करना पड़ता है
  • दूसरी भाषाओं में console.log("abc") जैसे function से सरल IO किया जा सकता है

    • इस बात पर सवाल है कि Haskell के IO में इससे क्या अंतर है
  • जिन्होंने Haskell को आज़माया नहीं है, उन्हें GHC extensions के साथ इस्तेमाल होने वाला वास्तविक Haskell बहुत जटिल लग सकता है

    • इससे Haskell के प्रति रुचि कम हो सकती है