Algebraic Effects की ज़रूरत क्यों है
(antelang.org)- Algebraic Effects (effect handlers) एक लचीला control flow tool हैं, जिनकी मदद से language-level features जैसे exception handling, generators, coroutines आदि को library स्तर पर implement किया जा सकता है
- इन्हें functional programming में आम context management, dependency injection, global state के विकल्प आदि में भी लागू किया जा सकता है
- ये API design को सरल बनाने और code के भीतर state/environment pass करने की प्रक्रिया को automate करने में मदद करते हैं
- इनके फायदे में functional purity की गारंटी, replayability, security audit आदि का समर्थन भी शामिल है
- हाल की compiler तकनीकों में प्रगति के कारण performance issues भी काफी हद तक सुधर चुके हैं
Algebraic Effects का परिचय
Algebraic Effects (जिन्हें effect handlers भी कहा जाता है) हाल के समय में तेज़ी से ध्यान आकर्षित करने वाला programming language feature हैं। Ante और कई research languages (Koka, Effekt, Eff, Flix आदि) में यह एक core feature के रूप में तेज़ी से फैल रहा है। बहुत-सी सामग्री effect handlers की अवधारणा समझाती है, लेकिन वास्तव में इनकी ज़रूरत "क्यों" है, इस पर गहराई से चर्चा कम मिलती है। यह लेख Algebraic Effects के व्यावहारिक उपयोग और फायदों को यथासंभव व्यापक रूप से परिचित कराता है।
Syntax और semantics को जल्दी समझें
- Algebraic Effects का विचार "resume किए जा सकने वाले exceptions" जैसा है
effect SayMessageकी तरह effect function declare किए जा सकते हैंfoo () can SayMessage = ...की तरह यह बताया जा सकता है कि कोई function उस effect का उपयोग कर सकता हैhandle foo () | say_message () -> ...के जरिए exception के try/catch की तरह handling की जा सकती है
इस बुनियादी संरचना के माध्यम से effect calls और control को संभाला जा सकता है
User-defined control flow का विस्तार
Algebraic Effects की सबसे बड़ी वजह यह है that एक ही language feature की मदद से वे क्षमताएँ, जिनके लिए पहले अलग-अलग language features (generator, exception, coroutine, async आदि) चाहिए होते थे, library के रूप में implement की जा सकती हैं।
- यदि किसी function में polymorphic effect variable (
can e) हो, तो अलग-अलग effects को function arguments के रूप में pass और combine किया जा सकता है - उदाहरण के तौर पर,
mapfunction को इस तरह declare किया जा सकता है कि argument के रूप में दिया गया function कोई भी effecteइस्तेमाल कर सके, जिससे output, async आदि विभिन्न effects के साथ उसका स्वाभाविक संयोजन संभव हो जाता है
Exception और generator implementation के उदाहरण
- Exception implementation: effect होने के बाद यदि
resumecall किए बिना handle किया जाए, तो यह exception की तरह काम करता है - Generator implementation:
Yieldeffect define करके, हर बार value yield होने पर बाहरी handler बीच में आकर condition के अनुसार flow को control कर सकता है; filtering जैसे advanced patterns भी अपेक्षाकृत सरल code से लिखे जा सकते हैं
कई effects को मिलाकर इस्तेमाल कर पाना भी, मौजूदा effect abstraction techniques की तुलना में, एक बड़ा फायदा है
Abstraction layer के रूप में उपयोग
Algebraic Effects केवल core programming features को बढ़ाने तक सीमित नहीं हैं; ये कई business application scenarios में भी काफ़ी उपयोगी हैं
Dependency Injection
- database, output आदि dependencies को effects के रूप में abstract करके handlers से manage किया जा सकता है
- testing के लिए mock objects का विकल्प, output redirection आदि भी लचीले ढंग से implement किए जा सकते हैं
Conditional logging या output management
- logging level के अनुसार log messages को print करना है या नहीं, इसे centrally control किया जा सकता है
API design को सरल बनाना और context passing को automate करना
State effect का उपयोग
- जहाँ Context object या environment information pass करनी हो, वहाँ effect-based
get/setके जरिए implementation करने पर explicit passing के बिना state management automate किया जा सकता है - पहले हर function में context को argument के रूप में pass करना पड़ता था, लेकिन state effect से इस हिस्से को छिपाया जा सकता है
Global objects का विकल्प
- random number generator, memory allocation जैसी state, जिन्हें पहले global objects से manage किया जाता था, उन्हें भी effect के रूप में abstract किया जा सकता है; इससे code clarity, testability और concurrency support में फायदा मिलता है
- सिर्फ handler बदलकर वास्तविक random source को लचीले ढंग से बदला जा सकता है
Direct style में लिखने का समर्थन
- पहले option types, error wrapping आदि के कारण कई objects को nested रूप में handle करना पड़ता था
- effects ऐसे wrapping के बिना भी error या side-effect paths को साफ़ तरीके से व्यक्त कर सकते हैं
Purity की गारंटी और security audit
Side effects को स्पष्ट करना
- ज़्यादातर effect handler languages में, side effects पैदा करने वाले functions की type signature में
can IO,can Printजैसे effects को अनिवार्य रूप से स्पष्ट करना पड़ता है - thread creation, software transactional memory (STM) आदि में pure functions अनिवार्य होते हैं
Log replay और deterministic networking
- purity के आधार पर
record,replayजैसे handlers बनाकर execution results को दोबारा reproduce किया जा सकता है - debugging, databases, game networking आदि में deterministic results और rollback support उपलब्ध कराया जा सकता है
Capability-based Security का समर्थन
- function type signatures में सभी unhandled effects सामने दिखाई देते हैं, इसलिए external library की security audit में यह प्रभावी होता है
- यदि पहले side effects से मुक्त कोई function update होकर
can IOजोड़ ले, तो उसे call करने वाला code इसे तुरंत पकड़ सकता है
हालाँकि, चूँकि सभी effects अपने-आप propagate होते हैं, इसलिए अनजाने में effects के handle हो जाने जैसे side effects भी हो सकते हैं
Efficiency के नज़रिए से और निष्कर्ष
- पहले execution efficiency एक कमज़ोरी मानी जाती थी, लेकिन हाल में tail-resumptive effects जैसे कई मामलों में optimization काफ़ी आगे बढ़ चुकी है
- अलग-अलग languages में उपयुक्त compiler strategies (closure call, evidence passing, handler specialization आदि) लागू की जा रही हैं
यह अपेक्षा की जा रही है कि Algebraic Effects भविष्य की programming languages में कहीं अधिक केंद्रीय स्थान हासिल करेंगे।
1 टिप्पणियां
Hacker News राय
मुझे इसमें दो कमियां दिखती हैं
दिए गए कोड हिस्से को देखें तो,
fooयाbarके fail हो सकने का कोई संकेत बिल्कुल नहीं है, यह पहली बात हैयह जानने के लिए कि ऐसे calls error handler को trigger कर सकते हैं, type signature खुद जाकर देखनी पड़ती है, और स्थिति के हिसाब से IDE की मदद के बिना हाथ से भी करना पड़ सकता है
दूसरी बात, जब यह पता चल जाए कि
fooऔरbarfail हो सकते हैं, तब असल fail होने पर कौन-सा code चलेगा यह खोजने के लिए call stack में काफी ऊपर जाकरwithexpression ढूंढना पड़ता है, और उसके बाद उस handler को नीचे तक follow करना पड़ता हैइस behavior को statically follow करना या IDE में सीधे definition पर jump करना संभव नहीं है, क्योंकि
my_functionकई जगहों से अलग-अलग handlers के साथ call हो सकता हैमुझे यह concept बहुत नया और ताज़ा लगता है, लेकिन नतीजे में code readability और debugging के लिहाज़ से चिंता होती है
run fail होने पर कौन-सा code चलता है, इस समस्या पर कहा गया कि यही dynamic code injection का मूल है
shallow-binding,deep-bindingजैसी कई dynamic features की तरह इसमें भी binding call stack के साथ होती हैstatic analysis या IDE jump संभव न होना भी इसी dynamic nature की वजह से है
लेकिन उनका मानना है कि व्यवहार में इसकी इतनी चिंता करने की ज़रूरत नहीं होती
क्योंकि यह pure code में effects जोड़ने का तरीका है, इसलिए context के हिसाब से pure effect हो या impure effect, test mocks या production environment जैसी अलग-अलग स्थितियों में जोड़ा जा सकता है
सिद्धांत कुछ-कुछ dependency injection जैसा है
पारंपरिक monad में भी इसे मिलते-जुलते तरीके से implement किया जा सकता है, लेकिन वहाँ भी monad वास्तव में कहाँ instantiate होता है यह जानने के लिए call stack देखना पड़ता है
इन techniques के फायदे हैं, लेकिन उनकी कीमत भी साफ़ है
testing और sandboxing के लिए ये अच्छे हैं, लेकिन code में क्या हो रहा है यह उतना स्पष्ट नहीं दिखता
lexical effects और handlers के लिए IDE support पर bachelor thesis लिखने का अनुभव साझा किया
उनका मानना है कि ऊपर उठाए गए सभी points पूरी तरह संभव हैं
पेपर लिंक
.NET ecosystem में interfaces के ज़रूरत से ज़्यादा इस्तेमाल की प्रवृत्ति का ज़िक्र, जहाँ method implementation तक सीधे jump करने के लिए कई चरणों से गुजरना पड़ता है
कई बार implementation किसी दूसरे assembly में हो तो IDE features लगभग बेकार हो जाते हैं
advanced Dependency Injection, खासकर Autofac, में LISP के dynamic scope variables की तरह hierarchical scopes बनाए जाते हैं, ताकि runtime पर यह तय हो कि service किस instance से bind होगी
इस नज़रिए से, effect को
ISomeEffectHandlerजैसे interface instance के रूप में inject करके effect होने पर उसी method call से represent किया जा सकता हैhandler का concrete behavior, जैसे exception throw करना या logging, DI configuration के आधार पर dynamically तय होगा
पहले exception
throwकरने वाला pattern इस्तेमाल होता था, लेकिन design को इस तरह बदला जा सकता है कि interface के आधार पर effects स्पष्ट हों और handling पूरी तरह DI पर छोड़ी जाएyieldजैसी iterator-संबंधित चीज़ों तक वे गहराई से नहीं गएउनका मानना है कि
fooऔरbarके fail हो सकने का कोई संकेत न होना ही असली point हैdirect style में effectful context की चिंता किए बिना code लिखा जा सकता है
fail होने पर कौन-सा code चलेगा, यह खोजना भी abstraction का ही स्वभाव है
runtime पर कौन-सा effect handler वास्तव में जुड़ता है, यह बाद में तय होता है
ठीक वैसे ही जैसे
f : g:(A -> B) -> t(A) -> Bमेंgचलते समय कौन-सा code चलेगा, यह पहले से नहीं पता होताcall stack में ऊपर जाकर handler खोजने की वजह से static analysis असंभव है, इस दावे से असहमति
राय यह है कि वास्तव में static analysis संभव है, और IDE में "go to caller" जैसी सुविधा से देखा जा सकता है कि कौन-से handlers इस्तेमाल हो रहे हैं
Ante का "pseudocode" बहुत प्रभावशाली लगा
Haskell के गुण और Elixir की expressive power व practicality का बहुत सुंदर मेल लगता है
यह developers के लिए Haskell जैसा महसूस होता है
compiler के mature होने की उम्मीद
Ante में app development करके देखना चाहते हैं
AE (Algebraic Effects) control flow को generalize करके coroutines भी implement कर सकता है, इस दावे पर
राय यह है कि किसी नई language runtime में AE implement करने का सबसे सरल तरीका शायद coroutines का इस्तेमाल कर
yield/resumeकी basic structure पर effects को syntactic layer की तरह चढ़ाना होगापूछा गया कि क्या इसमें कुछ छूट रहा है
AE और coroutines के बीच एक बड़ा अंतर type safety बताया गया
AE में source code स्तर पर यह बताया जा सकता है कि कोई function कौन-से effects इस्तेमाल कर सकता है
उदाहरण के लिए,
query_db(): User can Databaseजैसा type हो तो इसका मतलब है कि वह database access कर सकता है, और उसे call करते समयDatabasehandler देना ज़रूरी होगाक्या किया जा सकता है और क्या नहीं, इसकी पाबंदियाँ बहुत साफ़ दिखाई देती हैं
जैसे NextJS में server components सीधे client features इस्तेमाल नहीं कर सकते, वैसे ही इस तरह की safety constraints कई क्षेत्रों में लोकप्रिय हैं
Effect-TS, JavaScript में इस approach (coroutines के उपयोग) के काफ़ी करीब आता है, लेकिन यह सच में अच्छा विचार है या नहीं, इस पर भरोसा नहीं
Spring framework की DI की तरह, चिंता यह है कि AE पूरे codebase में फैलकर उल्टा complexity बढ़ा सकता है
आलोचना यह भी कि EffectDays में frontend effects के इस्तेमाल पर ज़्यादातर talks लगभग बेकार boilerplate से भरी थीं
AE एक आकर्षक concept है, लेकिन बहुत-सी चीज़ों को functions में wrap करना पड़े तो JavaScript की सहज coding style कमजोर पड़ सकती है
दूसरी ओर,
motioncanvasजैसे उदाहरण, जहाँ सिर्फ coroutines से जटिल 2D graphics scenarios आसानी से लिखे जा सकते हैं, अपने आप में बड़ा फ़ायदा हैंसंबंधित वीडियो EffectDays
MotionCanvas
दावा किया गया कि thread के भीतर AE handler,
call/ccकी तरह code को कई बारresumeकर सकता हैजबकि coroutine में हर
yieldके बाद आम तौर पर सिर्फ एक बार ही resume किया जा सकता हैऐसी अनिश्चित execution flow भविष्यवाणी को और कठिन बना देती है, इसलिए वे ऐसे functions को explicitly return करना पसंद करते हैं जिन्हें कई बार call किया जा सके, या iterator जैसी दूसरी structures का उपयोग करना बेहतर मानते हैं
coding abstraction के रूप में यह concept बहुत आकर्षक लगता है
Sun में kernel programming करते समय,
sleep(foo)जैसे call के बादfooकी वजह से फिर से जागने पर code को succinct तरीके से लिखा जा सकता है, यह बड़ा लाभ लगता हैcontrol flow से हर edge case को अलग-अलग संभालने का बोझ कम हो जाता है
memory locality जैसी समस्याओं पर ध्यान रहे, तो कई functions को पहले से waiting state में initialize करके, algorithm को हर unit की mutation के रूप में सीधे व्यक्त करने में मज़ा आ सकता है
"algebraic effects resumable exceptions जैसे हैं" इस दावे पर
पूछा गया कि यह
ApplicativeErrorयाMonadErrortype classes से व्यवहारिक रूप से किस तरह अलग हैfunction किन effects का उपयोग कर सकता है, यह बताने का तरीका checked exceptions जैसा लगता है, और
handleexpression से effect को संभालना भी लगभगtry/catchजैसा हैये type classes पहले से
handleError/handleErrorWithजैसी methods के ज़रिए exceptions पकड़ने का तरीका देती हैंकहा गया कि algebraic effects को भले "भविष्य" की languages की चीज़ कहा जाए, लेकिन व्यवहार में यह concept आज भी काफ़ी इस्तेमाल हो रहा है
cats विवरण लिंक
अगर सिर्फ एक effect की बात हो तो बड़ा अंतर न हो, लेकिन जब कई effects एक साथ चाहिए हों, तब direct effect support, monad को explicitly nest करने की तुलना में कहीं अधिक साफ़ और intuitive होता है
monad combine करने पर ordering, या कुछ functions के results का अपेक्षित monad set से न मिलना जैसी झंझटें आती हैं, जिनमें क्रम बदलने जैसी परेशानियाँ शामिल हैं
व्यक्तिगत रूप से उनका मानना है कि monad और effects को प्रतिस्पर्धी नहीं बल्कि एक-दूसरे के पूरक नज़रिए के रूप में देखना ज़्यादा सही है
संबंधित पेपर देखें (उदाहरण: Koka पेपर)
algebraic effects,
delimited continuationकी तरह program stack पर काम करते हैंसिर्फ साधारण monad tricks से stack frame में 5 स्तर ऊपर मौजूद effect handler तक तुरंत jump करके, वहाँ सिर्फ local variables बदलना और फिर 5 स्तर नीचे लौट आना संभव नहीं होता
अंतर static बनाम dynamic behavior का है
monad के साथ programming करते समय सारे संबंधित methods खुद implement करने पड़ते हैं, लेकिन effect system में किसी भी समय dynamically effect handler install करके मौजूदा handler को लचीले ढंग से override किया जा सकता है
उदाहरण के लिए, testing के लिए नीचे की layer में IO traits वाला खास monad इस्तेमाल करना, और उससे भी नीचे effect handler install करना जैसी composite structure संभव है
समानताएँ काफ़ी हैं, लेकिन usability में फ़र्क बड़ा है
algebraic effects,
freemonad जैसी बनावट रखते हैं, लेकिन built-in होने की वजह से syntax आसान है और composability बेहतर हैHaskell जैसी monad-केंद्रित languages में type class inference (
mtlstyle) और built-in bind syntax की वजह से ऊपर-ऊपर से मिलती-जुलती effect मिल सकती हैपहले यह ग़लतफ़हमी थी कि algebraic effects सिर्फ static type systems में ही आते हैं, लेकिन हाल में पता चला कि इनके dynamic रूप भी हैं
Eff के dynamic version पर दो लेख खास तौर पर प्रभावशाली लगे (पहला, दूसरा)
"generalized arity की parameterized operations" जैसे विचार, abstraction को programming से जोड़ने के लिहाज़ से भी रोचक लगे
यह टिप्पणी कि पुराना concept हाल में नए नाम और नए फ्रेम के साथ फिर सामने आ रहा है
LISP Condition System परिचय
Algebraic Effects का अनुभव
OCaml 5 alpha में effects के साथ protohackers करने का अनुभव
मज़ेदार था, लेकिन उस समय toolchain कुछ असुविधाजनक थी
Ante उससे मिलता-जुलता लगता है, इसलिए आगे के विकास की उम्मीद है
अभी type system जुड़ा नहीं है, लेकिन अब चीज़ें काफ़ी साफ़-सुथरी लगती हैं
Prolog में बहुत समय बिताने के बाद, ऐसी language की तलाश है जो nondeterministic function composition और compile-time type checking को आसान बना सके
Ante भी संभावित उम्मीदवारों में से एक लगती है
यह टिप्पणी भी कि LSP, tree-sitter जैसे developer tools और editor plugins को नहीं भूलना चाहिए
नई language के लिए शुरू से tooling को ज़रूरी मानते हैं
debugging experience भी महत्वपूर्ण है, इसलिए कम-से-कम debug mode में replayability को default capability की तरह देना संभव है या नहीं, यह भी देख रहे हैं
"algebraic effects resumable exceptions जैसे हैं" इस दावे पर
पूछा गया कि क्या यह Common Lisp conditions जैसा है
पुरानी चीज़ का सिर्फ नया नाम बनकर लौटना दिलचस्प लगा
algebraic effects, LISP condition system से कहीं अधिक व्यापक हैं
continuations के multi-shot हो सकने की वजह से यह Scheme के
call/ccसे मिलता हैयह भी कहा गया कि ऐसी parallelism कभी-कभी न होने से भी बुरी स्थिति पैदा कर सकती है
Smalltalk में "resumable exceptions" होते हैं
यदि effects को सिर्फ पुराने condition system का renamed version मान लिया जाए, तो चर्चा आगे बढ़ना कठिन हो जाता है
आज जिन algebraic effects पर चर्चा हो रही है, वे साधारण concept से अधिक वास्तविक अंतर रखते हैं
Dependency Injection का ज़िक्र भी इसी तरह के संदर्भ में किया जा सकता है