Prism: टाइप्ड effects वाली एक non-pure functional भाषा
(stephendiehl.com)- Prism एक experimental compiler है जो mutable variables, exceptions, और streams जैसे effects को छिपाने के बजाय type में दिखाता है, जबकि बाहर से observe न होने वाले local mutations को
Int -> Intजैसे pure type के रूप में बनाए रखता है - इसका core algebraic effect handlers और row polymorphism है, जहाँ effects function type की row में जुड़ते हैं और handlers सिर्फ ज़रूरी labels को handle करके बाकी आगे pass करते हैं
- यही effect system exceptions, generators/streams, lenses,
varstate, औरfail/guardflow को express करता है, और कुछ paths में बिना intermediate lists या runtime composition के lower हो जाता है - performance के लिए यह evidence passing और Perceus reference counting को जोड़ता है ताकि हर effect call पर allocation से बचा जा सके, और uniquely owned values की functional updates को in-place mutation में optimize किया जा सके
- Prism LLVM IR, MLIR, C runtime, Rust interpreter, Lean 4 model, और WASM playground भी देता है, जिससे type inference और lowering results को सीधे देखा जा सकता है
बाहर से अदृश्य mutation और typed effects
- Prism की शुरुआत इस विचार से होती है कि अंदर
varऔर assignment इस्तेमाल करने वालाfibfunction भी बाहरी observer को pure function जैसा दिख सकता है- उदाहरण
fibदो variables को in-place update करता है, लेकिन function के बाहर state expose नहीं करता - इसका type
Int -> Intहै, और effect type में नहीं दिखता
- उदाहरण
- Prism पिछले 3 साल में विकसित किया गया एक proof-of-concept functional compiler है, जो OCaml 5, Haskell, और Koka परिवार के modern type ideas पर आधारित होकर effects को model करता है
- मुख्य दिशा effects से बचना नहीं, बल्कि उन्हें type system में लाना और compiler से उनकी cost optimize कराना है
effects interface की तरह काम करते हैं
- Prism में effects, operations declare करते हैं और handlers उन operations को meaning देते हैं — यह algebraic effect handlers का तरीका है
effect Gen { ctl yield(Int) : Unit }yieldoperation declare करता हैfn produce(n) : !{Gen} Unitमें!{Gen}type में यह दिखाता है कि functionyieldकरता है
- वही producer continuation
kको कैसे handle करता है, इसके आधार पर अलग-अलग अर्थ ले सकता हैtotalyieldvalues को जोड़ता हैcountyieldकी संख्या गिनता हैkको फेंक देने पर exception, एक बार call करने पर state या generator, और कई बार call करने पर search behavior बनता है
Ambeffect का उदाहरणchooseऔरrejectसे Pythagorean triples की खोज को express करता हैchoose(n)0..n-1range के values देता है- handler हर candidate के लिए उसी continuation को resume करके search tree बनाता है
- OCaml 5 के विपरीत Prism effects को type में शामिल करता है, और Haskell के monad transformer stack की तरह layers को manually उठाने की ज़रूरत नहीं होती
- effect rows calls के दौरान structurally merge होती हैं
- यह stack नहीं बल्कि labels के set की तरह काम करती हैं
एक ही mechanism से व्यक्त होने वाली सुविधाएँ
-
exceptions
- Prism में exception एक continuation को discard करने वाला handler है
final ctlclausekको छोड़ देता है और उस body value को handler result बना देता है- यह
Resultpropagate करने या call stack में?बिखेरने वाला तरीका नहीं है - exception effect row का label है, इसलिए यह extensible exceptions की तरह compose होता है
- हर failure एक अलग operation है, और function type की row यह दिखाती है कि कौन-से exceptions बाहर निकल सकते हैं
AbortऔरTimeoutके उदाहरण मेंfetchका type!{Abort, Timeout}हैwith_defaultसिर्फTimeoutको handle करके fallback"cached"लौटाता है, और handle होने के बाद type में सिर्फ!{Abort}बचता है- Java के checked exceptions की तरह class hierarchy से बंधा नहीं, बल्कि open structural set की तरह काम करता है
-
generators और streams
- stream एक ऐसा producer है जो
emitकरता है, एक transformer जो उसे पकड़कर फिर emit करता है, और एक consumer जो fold करता है - pipeline एक single producer के आसपास nested handlers के रूप में बनती है, इसलिए intermediate lists संरचनात्मक रूप से बनती ही नहीं
- उदाहरण:
srange(1, n).smap(square).skeep(even).stake(5).ssum() stake(5)जैसा early stop, ज़रूरी values मिलने के बाद continuation को discard करने वाला handler है- stream library पर Haskell के pipes और conduit का प्रभाव है
- stream एक ऐसा producer है जो
-
lenses
- Prism में lenses कोई अलग library नहीं, बल्कि record update paths और memory model का संयोजन हैं
- nested records में एक ही path expression से कई deep fields update किए जा सकते हैं
- उदाहरण:
{ g | player.pos.x = 30, player.hp = 95, score = 110 } - nested record की spine फिर से बनाई जाती है, लेकिन अगर value uniquely owned हो तो टूटे हुए cells को reuse किया जाता है
- इसी संरचना की वजह से functional update को pointer writes में compile किया जा सकता है
- runtime पर optic types allocate या compose नहीं किए जाते
deriving (Lens)score_of,with_scoreजैसे सामान्य function accessors बना देता है
-
mutable state
varको private effect केget/setoperations में desugar किया जाता है- block के अंत में install किया गया handler इस effect को process करता है
- closure escape analysis उस स्थिति को reject करता है जहाँ state बाहर निकल सकती हो
- enclosing function empty effect row बनाए रख सकता है
-
failure
- failure को anonymous
Faileffect के रूप में व्यक्त किया जाता है, और type system यह संभालता है कि “यह expression value produce न भी कर सके” fail()failure करता है, औरguard(cond)condition false होने पर failure करता है??बाएँ पक्ष के fail होने पर fallback इस्तेमाल करता है?.option chain को follow करते हुए short-circuit करता है- comprehension guard crash की जगह failed elements को prune करता है
- क्योंकि
varभी handler sugar है,transactblock live variables का snapshot लेकर failure पर rollback कर सकता है
- failure को anonymous
modern type features
- Prism अधिकतर code में type signatures लिखे बिना काम करने के लिए Dunfield-Krishnaswami शैली की bidirectional higher-rank type inference का उपयोग करता है
- जहाँ higher-order polymorphism ज़रूरी हो, वहाँ
forallसे binders को explicitly लिखा जाता हैpick(g : forall a. (a) -> a)मेंgकोBoolऔरIntदोनों पर apply किया जा सकता है- Damas-Milner core में पहली call पर
aकाBoolसे unify हो जाना दूसरी call को reject कर देता
- ad-hoc polymorphism को Lean-style type classes से व्यक्त किया गया है
- instances named values हैं
given Ord(a)dictionary की माँग करता है- कई instances होने पर एक को
canonicalचिह्नित किया जाता है और बाकी कोusingसे explicitly चुना जाता है
derivingEq,Ord,Show,Lensजैसी repetitive instances और field accessors generate करता है- pattern matching भी classes से जुड़ा हुआ है
pattern First(n) for Peek = view peekएक ऐसा pattern है जो class method को view की तरह इस्तेमाल करता हैhead_orएक ही pattern को कई ऐसे types पर इस्तेमाल कर सकता है जिनके पासPeekinstance हो
showअलग class के बिना type-directed तरीके से काम करता है- compiler static type से structural printer synthesize करता है
- output format तय करने के लिए runtime type tags नहीं पढ़े जाते
- effect rows भी polymorphic हैं
fn twice(f : (Unit) -> Int ! {| e})argument function की effect roweचाहे जो हो, result को जोड़ता है- pure thunk में
eempty row{}से unify हो जाता है TickयाSayजैसे effects करने वाले thunk में वही row आगे pass होती है
effect cost घटाने वाली compilation technique
- textbook algebraic effect implementations computation को free monad जैसी tree संरचना में बदल सकती हैं और हर operation पर छोटे cells allocate कर सकती हैं
- Prism का fast path Koka-शैली का evidence passing है
- computation को reconstruct करके handler खोजने के बजाय active handler clauses को हर operation site तक सामान्य parameters की तरह pass किया जाता है
do opसीधे function call में lower हो जाता है- handler install करते समय सिर्फ एक closure allocate होती है, इसलिए cost
O(handlers)रहती है, operations की संख्या के अनुपात में नहीं
- free monad encoding fallback के रूप में बनी रहती है
- data structure में escape होने वाली computations
- असली multishot resumption
- masked handler जैसे patterns इसके target हैं
- stream pipelines में interprocedural flow analysis ज़रूरी effect evidence को function boundaries के पार तक compute करता है
- producer और transformer के बीच evidence thread किया जाता है
- पूरी chain single loop में lower हो जाती है
- न intermediate lists बनती हैं, न per-operation cell allocation
- यह संरचना Haskell की stream fusion या Rust iterator adapters के monomorphization जैसे परिणाम effect compiler में हासिल करती है
memory model और runtime
- Prism Perceus reference counting का उपयोग करता है
- heap cells statically known points पर deterministically free होते हैं
- कोई pause या tracing नहीं है
- frame-limited reuse pattern match से अभी-अभी टूटे हुए cell को constructor side में वापस दे देता है
- uniquely owned list पर
mappure function की तरह नई list लौटाता हुआ दिखता है, लेकिन वास्तव में in-place mutate हो सकता है - lens updates का pointer writes में compile होना भी इसी mechanism का हिस्सा है
- uniquely owned list पर
- runtime structure Lean 4 की memory discipline जैसी है, लेकिन Prism LLVM IR emit करता है
- LLVM IR
inkwellके जरिए generate होता है - उसी program के लिए text MLIR module भी generate होता है
- result को
clangसे hand-written C runtimeprism_rt.cके साथ link किया जाता है
- LLVM IR
prism_rt.cलगभग 2000 lines का छोटा runtime है- heap cells का रूप
{ refcount, tag, arity, fields... }वाले 4 या अधिक word structure जैसा है - इसमें allocator,
rc_inc/rc_dec, in-place reuse allocator, bignum और string primitives शामिल हैं - कोई collector thread, card table, या safepoint नहीं है
- heap cells का रूप
- default allocator libc
mallocहै, और benchmarking के लिए opt-inmimallocconfig उपलब्ध है - live-cell oracle program समाप्ति पर heap balance 0 होने की assert करता है, जिससे test suite garbage-free गुण की जाँच करता है
Lean model और backend verification
- Prism interpreter CEK machine परिवार का है, और यह machine Lean 4 model
models/Prism.leanमें reflect की गई है - Lean 4 model में small-step relation deterministic होने का machine-checked theorem है
- program ठीक एक ही next state में आगे बढ़ता है
- Rust implementation का interpreter differential oracle के रूप में भी इस्तेमाल होता है
- corpus के सभी programs interpreter, LLVM, MLIR, और C-linked binary backends से गुजरते हैं
- build pass होने के लिए चारों outputs byte-identical होने चाहिए
- determinism replayable durable execution की नींव बनता है
- विचार यह है कि input fix करके execution record किया जाए तो bit-for-bit replay संभव हो
- future versions में crash के बाद replay safety को type property की तरह check करने वाला Prism कल्पित है
WASM playground और source
- यही interpreter WASM में compile किया गया है, इसलिए playground में बिना installation के Prism code चलाया जा सकता है
- playground programs को worker में चलाता है
- buttons के जरिए inferred type signatures, effect rows, और lowered core IR dump किए जा सकते हैं
- इससे देखा जा सकता है कि
varloops या stream pipelines वास्तव में किस रूप में lower होते हैं
- इससे देखा जा सकता है कि
- लेख के examples dropdown में शामिल हैं
- पूरा source, Lean model, और C runtime prism repository on GitHub में उपलब्ध हैं
implementation lineage और project का स्वभाव
- Prism का core IR Levy की Call-by-Push-Value: A Functional/Imperative Synthesis पर आधारित call-by-push-value परिवार का है
- fast effect path Xie और Leijen के Generalized Evidence Passing for Effect Handlers, Effect Handlers, Evidently के काफ़ी निकट है
- memory model Perceus: Garbage Free Reference Counting with Reuse, Reference Counting with Frame-Limited Reuse, FP^2: Fully in-Place Functional Programming पर आधारित है
- effect rows row polymorphism और scoped labels की परंपरा में हैं, और handlers Plotkin तथा Pretnar के Handlers of Algebraic Effects से Eff और Koka होते हुए आए रूप में हैं
- pattern matching decision tree और usefulness matrix पर आधारित है, और
patternsyntax view pattern तथा GHC के Pattern Synonyms का संयोजन है - failure hierarchy The Verse Calculus को
final ctlhandler से वापस पाने जैसा है - Prism की दिशा “pure functional” होने से ज़्यादा effects को visible, typed, और composably trackable बनाना है
- project खुद practical tool से ज़्यादा एक toy और artistic work के करीब है, और इसे functional programming ideas की intellectual beauty के लिए बनाए गए compiler के रूप में प्रस्तुत किया गया है
1 टिप्पणियां
Lobste.rs पर टिप्पणियाँ
समझ नहीं आता कि lenses का effects से क्या संबंध है। लेख में जब भी lenses का ज़िक्र आता है, “एक trick को पाँच तरीकों से” के तहत रखे जाने के अलावा वे आपस में असंबंधित लगते हैं
और
tick_use()आखिर करना क्या चाहता है, यह भी समझ नहीं आता। क्या लेखक उम्मीद कर रहे हैं कि पाठक बिना व्याख्या के ऐसा उलझा हुआ example समझ लेंगे? type annotations होते तो मदद मिलतीफिर भी रूपक के स्तर से आगे lenses का effects से क्या संबंध है, साफ़ नहीं दिखता। लेखक ने लिखा है:
इसे समझने में और समय लगाना पड़ेगा, लेकिन यह सच में सुंदर दिखता है
वाकई प्रभावशाली है। बल्कि इसी वजह से सोचता हूँ कि Diehl लेख के अंत में compiler को toy क्यों कहते हैं। यह elegance के नए स्तर को दिखाने वाला सफल proof of concept लगता है
मैं और विस्तार से देखना चाहूँगा कि call-by-push-value intermediate representation असल में कैसी दिखती है। खासकर यह कि join points को कैसे handle किया जाता है
CBPV में effects जोड़ने की theory पर papers रहे हैं। यह कहना काफी natural है कि computations के पास effect types होते हैं और values के पास नहीं, लेकिन Koka की evidence passing पर सीधे लागू करने लायक इतना concrete रूप मैंने नहीं देखा, इसलिए यह दिलचस्प है
कुल मिलाकर, Koka की तुलना में इसकी स्थिति क्या है, यह जानना चाहूँगा। FBIP, Perceus, और evidence passing को देखकर साफ़ है कि यह Koka के काम से काफी प्रेरित है, और साथ ही intermediate representation में CBPV का इस्तेमाल करने से यह निश्चित रूप से अलग भी है। बस कितना अलग है, यह साफ़ नहीं दिखता
यह काफी हद तक उस चीज़ जैसा दिखता है जिसे बनाने के लिए मैं लगातार समय निकालने की सोच रहा था। अच्छा है!
विषय से थोड़ा हटकर, Stephen का “write you a haskell” project कुछ साल पहले रुक गया था, इसका मुझे हमेशा थोड़ा अफसोस रहा। Prism के लिए tutorial-level implementation explanation आए तो अच्छा होगा
इस language में “impure” क्या है, यह जानना चाहता हूँ। यह शब्द सिर्फ title में आता है और body में फिर नहीं दिखता
लगता है सभी effects track किए जाते हैं, इसलिए effect-free functions अभी भी mathematical functions हैं। क्या मैं कुछ miss कर रहा हूँ?
fibdefinition में।var ximpure mutable variable है, औरlet xpure immutable variable हैआम तौर पर languages की features मानी जाने वाली चीज़ें—जैसे X language का
yield, Y language काthrow—इन्हें एक और interface के रूप में implement किया गया है, यह सच में शानदार हैside effects, exceptions, continuations जैसे कई control flows को एक abstraction के ऊपर बना पाना design questions को पूरी तरह नए तरीके से देखने का दिलचस्प तरीका है, और उम्मीद है कि यह और exploration और innovation में मदद करेगा या उन्हें प्रेरित करेगा। लगता है आने वाले कई सालों तक inspiration लेने के लिए मैं इसे फिर-फिर देखता रहूँगा