खोज…
मोनाड्स को समझना अभ्यास से आता है
यह विषय उन्नत एफ # डेवलपर्स के लिए मध्यवर्ती के लिए है
"मोनाड क्या हैं?" एक सामान्य प्रश्न है। इसका उत्तर देना आसान है लेकिन जैसे हम हिचकीर्स को आकाशगंगा में गाइड करते हैं, हमें लगता है कि हम उत्तर को नहीं समझते हैं क्योंकि हमें नहीं पता था कि हम उसके बाद क्या पूछ रहे हैं।
बहुतों का मानना है कि मोनाड्स को समझने का तरीका उनका अभ्यास करना है। प्रोग्रामर के रूप में हम आमतौर पर Liskov के प्रतिस्थापन सिद्धांत, उप-प्रकार या उप-वर्ग हैं के लिए गणितीय नींव की परवाह नहीं करते हैं। इन विचारों का उपयोग करके हमने जो प्रतिनिधित्व किया है उसके लिए एक अंतर्ज्ञान प्राप्त किया है। मोनाड्स के लिए भी यही सच है।
मोनाड्स के साथ आरंभ करने में आपकी सहायता करने के लिए यह उदाहरण दर्शाता है कि मोनाडिक पार्सर कॉम्बिनेटर लाइब्रेरी का निर्माण कैसे किया जाता है। यह आपको आरंभ करने में मदद कर सकता है लेकिन समझदारी आपके स्वयं के मोनैडिक पुस्तकालय को लिखने से आएगी।
पर्याप्त गद्य, कोड के लिए समय
पार्सर प्रकार:
// A Parser<'T> is a function that takes a string and position
// and returns an optionally parsed value and a position
// A parsed value means the position points to the character following the parsed value
// No parsed value indicates a parse failure at the position
type Parser<'T> = Parser of (string*int -> 'T option*int)
पार्सर की इस परिभाषा का उपयोग करते हुए हम कुछ मूलभूत पार्सर कार्यों को परिभाषित करते हैं
// Runs a parser 't' on the input string 's'
let run t s =
let (Parser tps) = t
tps (s, 0)
// Different ways to create parser result
let succeedWith v p = Some v, p
let failAt p = None , p
// The 'satisfy' parser succeeds if the character at the current position
// passes the 'sat' function
let satisfy sat : Parser<char> = Parser <| fun (s, p) ->
if p < s.Length && sat s.[p] then succeedWith s.[p] (p + 1)
else failAt p
// 'eos' succeeds if the position is beyond the last character.
// Useful when testing if the parser have consumed the full string
let eos : Parser<unit> = Parser <| fun (s, p) ->
if p < s.Length then failAt p
else succeedWith () p
let anyChar = satisfy (fun _ -> true)
let char ch = satisfy ((=) ch)
let digit = satisfy System.Char.IsDigit
let letter = satisfy System.Char.IsLetter
satisfy
एक समारोह है कि किसी भी है sat
समारोह एक पार्सर कि सफल होता है हम पारित नहीं किया है पैदा करता है EOS
वर्तमान स्थिति में और चरित्र गुजरता sat
कार्य करते हैं। satisfy
का उपयोग करके हम कई उपयोगी चरित्र पार्सर बनाते हैं।
FSI में यह चल रहा है:
> run digit "";;
val it : char option * int = (null, 0)
> run digit "123";;
val it : char option * int = (Some '1', 1)
> run digit "hello";;
val it : char option * int = (null, 0)
हमारे पास कुछ मौलिक पार्सर्स हैं। हम उन्हें पार्सर कॉम्बिनेटर फ़ंक्शन का उपयोग करके अधिक शक्तिशाली पार्सर में जोड़ देंगे
// 'fail' is a parser that always fails
let fail<'T> = Parser <| fun (s, p) -> failAt p
// 'return_' is a parser that always succeed with value 'v'
let return_ v = Parser <| fun (s, p) -> succeedWith v p
// 'bind' let us combine two parser into a more complex parser
let bind t uf = Parser <| fun (s, p) ->
let (Parser tps) = t
let tov, tp = tps (s, p)
match tov with
| None -> None, tp
| Some tv ->
let u = uf tv
let (Parser ups) = u
ups (s, tp)
नामों और हस्ताक्षरों को मनमाने ढंग से नहीं चुना गया है, लेकिन हम इस पर ध्यान नहीं देंगे, इसके बजाय आइए देखें कि हम पार्सर को अधिक जटिल लोगों में संयोजित करने के लिए bind
का उपयोग कैसे करते हैं:
> run (bind digit (fun v -> digit)) "123";;
val it : char option * int = (Some '2', 2)
> run (bind digit (fun v -> bind digit (fun u -> return_ (v,u)))) "123";;
val it : (char * char) option * int = (Some ('1', '2'), 2)
> run (bind digit (fun v -> bind digit (fun u -> return_ (v,u)))) "1";;
val it : (char * char) option * int = (null, 1)
यह हमें दिखाता है कि bind
हमें दो पार्सर को एक अधिक जटिल पार्सर में संयोजित करने की अनुमति देता है। जैसा कि bind
का परिणाम एक पार्सर है जो बदले में फिर से जोड़ा जा सकता है।
> run (bind digit (fun v -> bind digit (fun w -> bind digit (fun u -> return_ (v,w,u))))) "123";;
val it : (char * char * char) option * int = (Some ('1', '2', '3'), 3)
bind
वह मूलभूत तरीका होगा जिससे हम पार्सर्स को जोड़ते हैं हालांकि हम सिंटैक्स को सरल बनाने के लिए सहायक कार्यों को परिभाषित करेंगे।
वाक्यविन्यास को सरल बनाने वाली चीजों में से एक संगणना अभिव्यक्ति है । वे परिभाषित करना आसान है:
type ParserBuilder() =
member x.Bind (t, uf) = bind t uf
member x.Return v = return_ v
member x.ReturnFrom t = t
// 'parser' enables us to combine parsers using 'parser { ... }' syntax
let parser = ParserBuilder()
FSI
let p = parser {
let! v = digit
let! u = digit
return v,u
}
run p "123"
val p : Parser<char * char> = Parser <fun:bind@49-1>
val it : (char * char) option * int = (Some ('1', '2'), 2)
यह इसके बराबर है:
> let p = bind digit (fun v -> bind digit (fun u -> return_ (v,u)))
run p "123";;
val p : Parser<char * char> = Parser <fun:bind@49-1>
val it : (char * char) option * int = (Some ('1', '2'), 2)
एक और मौलिक पार्सर कॉम्बीनेटर है orElse
हम उपयोग करने जा रहे हैं, orElse
:
// 'orElse' creates a parser that runs parser 't' first, if that is successful
// the result is returned otherwise the result of parser 'u' is returned
let orElse t u = Parser <| fun (s, p) ->
let (Parser tps) = t
let tov, tp = tps (s, p)
match tov with
| None ->
let (Parser ups) = u
ups (s, p)
| Some tv -> succeedWith tv tp
यह हमें परिभाषित करने के लिए अनुमति देता है letterOrDigit
इस तरह:
> let letterOrDigit = orElse letter digit;;
val letterOrDigit : Parser<char> = Parser <fun:orElse@70-1>
> run letterOrDigit "123";;
val it : char option * int = (Some '1', 1)
> run letterOrDigit "hello";;
val it : char option * int = (Some 'h', 1)
> run letterOrDigit "!!!";;
val it : char option * int = (null, 0)
Infix ऑपरेटरों पर एक नोट
एफपी पर एक आम चिंता का विषय असामान्य इन्फिक्स ऑपरेटरों जैसे >>=
, >=>
, <-
और इसी तरह का उपयोग है। हालाँकि, अधिकांश +
, -
, *
, /
और %
के उपयोग को लेकर चिंतित नहीं हैं, ये जाने-माने संचालकों द्वारा मूल्यों की रचना के लिए उपयोग किए जाते हैं। हालांकि, एफपी में एक बड़ा हिस्सा न केवल मूल्यों बल्कि कार्यों के रूप में भी रचना करने के बारे में है। एक मध्यवर्ती FP डेवलपर के लिए infix ऑपरेटरों >>=
, >=>
, <-
अच्छी तरह से जाना जाता है और इसमें विशिष्ट हस्ताक्षर के साथ-साथ शब्दार्थ भी होना चाहिए।
हमने अब तक परिभाषित किए गए कार्यों के लिए, हम पार्सर्स को संयोजित करने के लिए उपयोग किए जाने वाले निम्नलिखित infix ऑपरेटरों को परिभाषित करेंगे:
let (>>=) t uf = bind t uf
let (<|>) t u = orElse t u
तो >>=
अर्थ है bind
और <|>
अर्थ है orElse
।
यह हमें पार्सर्स को अधिक सक्सेज करने की अनुमति देता है:
let letterOrDigit = letter <|> digit
let p = digit >>= fun v -> digit >>= fun u -> return_ (v,u)
कुछ उन्नत पार्सर कॉम्बिनेटरों को परिभाषित करने के लिए जो हमें अधिक जटिल अभिव्यक्ति को पार्स करने की अनुमति देंगे हम कुछ और सरल पार्सर कॉम्बिनेटरों को परिभाषित करते हैं:
// 'map' runs parser 't' and maps the result using 'm'
let map m t = t >>= (m >> return_)
let (>>!) t m = map m t
let (>>%) t v = t >>! (fun _ -> v)
// 'opt' takes a parser 't' and creates a parser that always succeed but
// if parser 't' fails the new parser will produce the value 'None'
let opt t = (t >>! Some) <|> (return_ None)
// 'pair' runs parser 't' and 'u' and returns a pair of 't' and 'u' results
let pair t u =
parser {
let! tv = t
let! tu = u
return tv, tu
}
हम many
और sepBy
को परिभाषित करने के लिए तैयार हैं जो अधिक उन्नत हैं क्योंकि वे विफल होने तक इनपुट पार्सर लागू करते हैं। फिर many
और sepBy
एकत्रित परिणाम देता है:
// 'many' applies parser 't' until it fails and returns all successful
// parser results as a list
let many t =
let ot = opt t
let rec loop vs = ot >>= function Some v -> loop (v::vs) | None -> return_ (List.rev vs)
loop []
// 'sepBy' applies parser 't' separated by 'sep'.
// The values are reduced with the function 'sep' returns
let sepBy t sep =
let ots = opt (pair sep t)
let rec loop v = ots >>= function Some (s, n) -> loop (s v n) | None -> return_ v
t >>= loop
एक सरल अभिव्यक्ति पार्सर बनाना
हमारे द्वारा बनाए गए टूल से हम अब 1+2*3
जैसे सरल भावों के लिए एक पार्सर को परिभाषित कर सकते हैं
पूर्णांक pint
लिए एक पार्सर को परिभाषित करके हम नीचे से शुरू करते हैं
// 'pint' parses an integer
let pint =
let f s v = 10*s + int v - int '0'
parser {
let! digits = many digit
return!
match digits with
| [] -> fail
| vs -> return_ (List.fold f 0 vs)
}
हम जितना हो सके उतने अंकों को पार्स करने की कोशिश करते हैं, नतीजा char list
। यदि सूची खाली है तो हम fail
, अन्यथा हम वर्णों को पूर्णांक में मोड़ देते हैं।
एफएसआई में परीक्षण pint
:
> run pint "123";;
val it : int option * int = (Some 123, 3)
इसके अलावा, हमें पूर्णांक मानों को संयोजित करने के लिए उपयोग किए जाने वाले विभिन्न प्रकार के ऑपरेटरों को पार्स करने की आवश्यकता है:
// operator parsers, note that the parser result is the operator function
let padd = char '+' >>% (+)
let psubtract = char '-' >>% (-)
let pmultiply = char '*' >>% (*)
let pdivide = char '/' >>% (/)
let pmodulus = char '%' >>% (%)
FSI:
> run padd "+";;
val it : (int -> int -> int) option * int = (Some <fun:padd@121-1>, 1)
सभी को एक साथ बांधना:
// 'pmullike' parsers integers separated by operators with same precedence as multiply
let pmullike = sepBy pint (pmultiply <|> pdivide <|> pmodulus)
// 'paddlike' parsers sub expressions separated by operators with same precedence as add
let paddlike = sepBy pmullike (padd <|> psubtract)
// 'pexpr' is the full expression
let pexpr =
parser {
let! v = paddlike
let! _ = eos // To make sure the full string is consumed
return v
}
यह सब FSI में चल रहा है:
> run pexpr "2+123*2-3";;
val it : int option * int = (Some 245, 9)
निष्कर्ष
परिभाषित करके Parser<'T>
, return_
, bind
और सुनिश्चित करें कि वे का पालन कर रही है monadic कानूनों हम एक सरल लेकिन शक्तिशाली Monadic पार्सर Combinator ढांचे का निर्माण किया है।
मोनाड्स और पार्सर्स एक साथ चलते हैं क्योंकि पार्सर्स को एक पार्सर राज्य में निष्पादित किया जाता है। मोनाडर्स हमें पार्सर स्थिति को छिपाते हुए पार्सर्स को संयोजित करने की अनुमति देता है और इस प्रकार अव्यवस्था को कम करता है और रचना की क्षमता में सुधार करता है।
हमने जो फ्रेमवर्क बनाया है वह धीमा है और कोई त्रुटि संदेश नहीं देता है, इस कारण कोड को सक्सेज रखने के लिए। FParsec दोनों स्वीकार्य प्रदर्शन के साथ-साथ उत्कृष्ट त्रुटि संदेश प्रदान करते हैं।
हालाँकि, अकेले एक उदाहरण मोनाड्स की समझ नहीं दे सकता है। एक मोनाड्स का अभ्यास करना है।
मोनाड्स पर यहां कुछ उदाहरण दिए गए हैं, जिन्हें आप अपनी समझ में लाने के लिए लागू करने की कोशिश कर सकते हैं:
- स्टेट मोनाड - छिपे हुए पर्यावरण राज्य को अंतर्निहित रूप से ले जाने की अनुमति देता है
- ट्रेसर मोनाड - ट्रेस स्थिति को स्पष्ट रूप से ले जाने की अनुमति देता है। राज्य मोनाड का एक प्रकार
- टर्टल मोनाड - टर्टल (लोगो) बनाने के लिए एक मोनाड। राज्य मोनाड का एक प्रकार
- निरंतरता मोनाड - एक कोरटाइन मोनाड। इसका एक उदाहरण F # में
async
है
सीखने के लिए सबसे अच्छी बात यह है कि आप जिस डोमेन में सहज हैं, उसके लिए मोनाड्स के लिए एक आवेदन पत्र लेकर आएं। मेरे लिए वह पारस था।
पूर्ण स्रोत कोड:
// A Parser<'T> is a function that takes a string and position
// and returns an optionally parsed value and a position
// A parsed value means the position points to the character following the parsed value
// No parsed value indicates a parse failure at the position
type Parser<'T> = Parser of (string*int -> 'T option*int)
// Runs a parser 't' on the input string 's'
let run t s =
let (Parser tps) = t
tps (s, 0)
// Different ways to create parser result
let succeedWith v p = Some v, p
let failAt p = None , p
// The 'satisfy' parser succeeds if the character at the current position
// passes the 'sat' function
let satisfy sat : Parser<char> = Parser <| fun (s, p) ->
if p < s.Length && sat s.[p] then succeedWith s.[p] (p + 1)
else failAt p
// 'eos' succeeds if the position is beyond the last character.
// Useful when testing if the parser have consumed the full string
let eos : Parser<unit> = Parser <| fun (s, p) ->
if p < s.Length then failAt p
else succeedWith () p
let anyChar = satisfy (fun _ -> true)
let char ch = satisfy ((=) ch)
let digit = satisfy System.Char.IsDigit
let letter = satisfy System.Char.IsLetter
// 'fail' is a parser that always fails
let fail<'T> = Parser <| fun (s, p) -> failAt p
// 'return_' is a parser that always succeed with value 'v'
let return_ v = Parser <| fun (s, p) -> succeedWith v p
// 'bind' let us combine two parser into a more complex parser
let bind t uf = Parser <| fun (s, p) ->
let (Parser tps) = t
let tov, tp = tps (s, p)
match tov with
| None -> None, tp
| Some tv ->
let u = uf tv
let (Parser ups) = u
ups (s, tp)
type ParserBuilder() =
member x.Bind (t, uf) = bind t uf
member x.Return v = return_ v
member x.ReturnFrom t = t
// 'parser' enables us to combine parsers using 'parser { ... }' syntax
let parser = ParserBuilder()
// 'orElse' creates a parser that runs parser 't' first, if that is successful
// the result is returned otherwise the result of parser 'u' is returned
let orElse t u = Parser <| fun (s, p) ->
let (Parser tps) = t
let tov, tp = tps (s, p)
match tov with
| None ->
let (Parser ups) = u
ups (s, p)
| Some tv -> succeedWith tv tp
let (>>=) t uf = bind t uf
let (<|>) t u = orElse t u
// 'map' runs parser 't' and maps the result using 'm'
let map m t = t >>= (m >> return_)
let (>>!) t m = map m t
let (>>%) t v = t >>! (fun _ -> v)
// 'opt' takes a parser 't' and creates a parser that always succeed but
// if parser 't' fails the new parser will produce the value 'None'
let opt t = (t >>! Some) <|> (return_ None)
// 'pair' runs parser 't' and 'u' and returns a pair of 't' and 'u' results
let pair t u =
parser {
let! tv = t
let! tu = u
return tv, tu
}
// 'many' applies parser 't' until it fails and returns all successful
// parser results as a list
let many t =
let ot = opt t
let rec loop vs = ot >>= function Some v -> loop (v::vs) | None -> return_ (List.rev vs)
loop []
// 'sepBy' applies parser 't' separated by 'sep'.
// The values are reduced with the function 'sep' returns
let sepBy t sep =
let ots = opt (pair sep t)
let rec loop v = ots >>= function Some (s, n) -> loop (s v n) | None -> return_ v
t >>= loop
// A simplistic integer expression parser
// 'pint' parses an integer
let pint =
let f s v = 10*s + int v - int '0'
parser {
let! digits = many digit
return!
match digits with
| [] -> fail
| vs -> return_ (List.fold f 0 vs)
}
// operator parsers, note that the parser result is the operator function
let padd = char '+' >>% (+)
let psubtract = char '-' >>% (-)
let pmultiply = char '*' >>% (*)
let pdivide = char '/' >>% (/)
let pmodulus = char '%' >>% (%)
// 'pmullike' parsers integers separated by operators with same precedence as multiply
let pmullike = sepBy pint (pmultiply <|> pdivide <|> pmodulus)
// 'paddlike' parsers sub expressions separated by operators with same precedence as add
let paddlike = sepBy pmullike (padd <|> psubtract)
// 'pexpr' is the full expression
let pexpr =
parser {
let! v = paddlike
let! _ = eos // To make sure the full string is consumed
return v
}
गणना अभिव्यक्तियाँ चेन मोनाड्स के लिए एक वैकल्पिक वाक्यविन्यास प्रदान करती हैं
मोनाड्स से संबंधित F#
कम्प्यूटेशन एक्सप्रेशन ( CE
) हैं। एक प्रोग्रामर आम तौर पर एक को लागू करता है CE
बजाय, चेनिंग monads लिए एक वैकल्पिक दृष्टिकोण प्रदान करने के लिए यानी इस की:
let v = m >>= fun x -> n >>= fun y -> return_ (x, y)
आप इसे लिख सकते हैं:
let v = ce {
let! x = m
let! y = n
return x, y
}
दोनों शैलियाँ समतुल्य हैं और यह डेवलपर की प्राथमिकता पर निर्भर है कि कौन सी चुनना है।
प्रदर्शित करने के लिए कि कैसे एक CE
को लागू करने के लिए एक सहसंबंध आईडी शामिल करने के लिए सभी निशान की तरह कल्पना करें। यह सहसंबंध आईडी उसी कॉल से संबंधित निशानों को सहसंबद्ध करने में मदद करेगी। यह बहुत उपयोगी है जब लॉग फाइल होती है जिसमें समवर्ती कॉल से निशान होते हैं।
समस्या यह है कि सभी कार्यों के तर्क के रूप में सहसंबंध आईडी को शामिल करना बोझिल है। जैसा कि मोनाड्स निहित स्थिति को ले जाने की अनुमति देता है, हम लॉग संदर्भ (यानी सहसंबंध आईडी) को छिपाने के लिए एक लॉग मोनाड को परिभाषित करेंगे।
हम एक लॉग संदर्भ और फ़ंक्शन के प्रकार को परिभाषित करके शुरू करते हैं जो लॉग संदर्भ के साथ होता है:
type Context =
{
CorrelationId : Guid
}
static member New () : Context = { CorrelationId = Guid.NewGuid () }
type Function<'T> = Context -> 'T
// Runs a Function<'T> with a new log context
let run t = t (Context.New ())
हम दो ट्रेस फ़ंक्शन को भी परिभाषित करते हैं जो लॉग संदर्भ से सहसंबंध आईडी के साथ लॉग इन करेंगे:
let trace v : Function<_> = fun ctx -> printfn "CorrelationId: %A - %A" ctx.CorrelationId v
let tracef fmt = kprintf trace fmt
trace
एक Function<unit>
जिसका अर्थ है कि जब यह लागू किया जाता है तो यह एक लॉग संदर्भ होगा। लॉग संदर्भ से हम सहसंबंध आईडी को उठाते हैं और इसे v
साथ v
इसके अलावा हम bind
और return_
को परिभाषित करते bind
और return_
कि वे return_
कानून का पालन करते हैं, यह हमारे लॉग मोनाड बनाता है।
let bind t uf : Function<_> = fun ctx ->
let tv = t ctx // Invoke t with the log context
let u = uf tv // Create u function using result of t
u ctx // Invoke u with the log context
// >>= is the common infix operator for bind
let inline (>>=) (t, uf) = bind t uf
let return_ v : Function<_> = fun ctx -> v
अंत में हम परिभाषित LogBuilder
है कि हम का उपयोग करने के लिए सक्षम हो जाएगा CE
श्रृंखला के लिए वाक्य रचना Log
monads।
type LogBuilder() =
member x.Bind (t, uf) = bind t uf
member x.Return v = return_ v
// This enables us to write function like: let f = log { ... }
let log = Log.LogBuilder ()
अब हम अपने कार्यों को परिभाषित कर सकते हैं जिनमें निहित लॉग संदर्भ होना चाहिए:
let f x y =
log {
do! Log.tracef "f: called with: x = %d, y = %d" x y
return x + y
}
let g =
log {
do! Log.trace "g: starting..."
let! v = f 1 2
do! Log.tracef "g: f produced %d" v
return v
}
हम जी के साथ निष्पादित करते हैं:
printfn "g produced %A" (Log.run g)
कौन सा प्रिंट:
CorrelationId: 33342765-2f96-42da-8b57-6fa9cdaf060f - "g: starting..."
CorrelationId: 33342765-2f96-42da-8b57-6fa9cdaf060f - "f: called with: x = 1, y = 2"
CorrelationId: 33342765-2f96-42da-8b57-6fa9cdaf060f - "g: f produced 3"
g produced 3
ध्यान दें कि CorrelationId को अंतर्निहित रूप से run
लिए g
से f
तक ले जाया जाता है जो हमें शूटिंग में परेशानी के दौरान लॉग प्रविष्टियों को सहसंबंधित करने की अनुमति देता है।
CE
में बहुत अधिक विशेषताएं हैं लेकिन इससे आपको अपने स्वयं के CE
: एस को परिभाषित करने में मदद मिलेगी।
पूर्ण कोड:
module Log =
open System
open FSharp.Core.Printf
type Context =
{
CorrelationId : Guid
}
static member New () : Context = { CorrelationId = Guid.NewGuid () }
type Function<'T> = Context -> 'T
// Runs a Function<'T> with a new log context
let run t = t (Context.New ())
let trace v : Function<_> = fun ctx -> printfn "CorrelationId: %A - %A" ctx.CorrelationId v
let tracef fmt = kprintf trace fmt
let bind t uf : Function<_> = fun ctx ->
let tv = t ctx // Invoke t with the log context
let u = uf tv // Create u function using result of t
u ctx // Invoke u with the log context
// >>= is the common infix operator for bind
let inline (>>=) (t, uf) = bind t uf
let return_ v : Function<_> = fun ctx -> v
type LogBuilder() =
member x.Bind (t, uf) = bind t uf
member x.Return v = return_ v
// This enables us to write function like: let f = log { ... }
let log = Log.LogBuilder ()
let f x y =
log {
do! Log.tracef "f: called with: x = %d, y = %d" x y
return x + y
}
let g =
log {
do! Log.trace "g: starting..."
let! v = f 1 2
do! Log.tracef "g: f produced %d" v
return v
}
[<EntryPoint>]
let main argv =
printfn "g produced %A" (Log.run g)
0