수색…


모나드를 이해하는 것은 연습에서 비롯됩니다.

이 항목은 중급에서 고급 F # 개발자를 대상으로합니다.

"모나드 란 무엇입니까?" 일반적인 질문입니다. 이것은 대답하기 쉽지만 Hitchhikers의 은하계 가이드 처럼 우리는 우리가 무엇을 요구했는지 알지 못했기 때문에 우리가 대답을 이해하지 못한다는 것을 깨닫습니다.

많은 사람들 은 모나드에 대한 이해가 그들을 연습함으로써 가능하다고 믿습니다. 프로그래머로서 우리는 일반적으로 Liskov의 대체 원칙, 하위 유형 또는 하위 클래스가 무엇인지에 대한 수학적 기반을 고려하지 않습니다. 이 아이디어를 사용함으로써 우리는 그들이 나타내는 것을 직감으로 얻었습니다. Monads에 대해서도 마찬가지입니다.

Monads를 시작하는 데 도움이되도록이 예제는 Monadic Parser Combinator 라이브러리를 작성하는 방법을 보여줍니다. 이것은 시작하는 데 도움이 될 수 있지만 자신의 모나 딕 (Monadic) 도서관을 작성하면 이해할 수 있습니다.

충분한 산문, 코드 시간

파서 타입 :

// 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)

Parser의이 정의를 사용하여 기본적인 파서 함수를 정의합니다

// 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

satisfysat 함수가 주어진 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 수있게합니다. 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' 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)

중서부 연산자에 대한 참고 사항

FP에 대한 공통 관심사는 >>= , >=> , <- 등등과 같은 특이한 중온 연산자를 사용하는 것입니다. 그러나 대부분은 + , - , * , /% 의 사용에 대해서는 관심이 없으며 값을 구성하는 데 사용되는 잘 알려진 연산자입니다. 그러나 FP에서 중요한 부분은 가치뿐만 아니라 기능도 구성하는 데 있습니다. 중간 FP 개발자에게 >>= , >=> , <- 중온 연산자는 잘 알려져 있으며 의미와 특정 서명이 있어야합니다.

지금까지 정의한 함수들에 대해, 파서를 결합하기 위해 사용 된 다음 중온 연산자를 정의 할 것입니다 :

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
  }

우리는 그들이 실패 할 때까지 입력 파서를 적용 할 때 훨씬 더 진보 된 manysepBy 를 정의 할 준비가되었습니다. 그런 다음 manysepBy 는 집계 된 결과를 반환합니다.

// '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 . 결과는 char list 입니다. 목록이 비어 있으면 fail . 그렇지 않으면 문자를 정수로 변환합니다.

FSI의 테스트 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 를 정의하고 그들이 모나드 법칙 을 따르는 지 확인함으로써 우리는 간단하지만 강력한 모나드 파서 결합 자 프레임 워크를 구축했습니다.

파서는 파서 (parser) 상태에서 실행되므로 모나드와 파서는 함께 사용됩니다. Monads를 사용하면 파서를 결합 할 수 있으며 파서 상태를 숨기므로 혼란을 줄이고 구성 가능성을 향상시킬 수 있습니다.

우리가 만든 프레임 워크는 느리고 코드를 간결하게하기 위해 오류 메시지를 생성하지 않습니다. FParsec 은 우수한 오류 메시지뿐만 아니라 수용 가능한 성능을 제공합니다.

그러나 모나드를 이해할 수는 없습니다. 하나는 모나드를 연습해야합니다.

다음은 Monads에 대한 이해를 돕기 위해 구현할 수있는 모나드 예제입니다.

  1. 상태 모나드 - 숨겨진 환경 상태를 암시 적으로 전달할 수 있습니다.
  2. Tracer Monad - 추적 상태를 암시 적으로 전달할 수 있습니다. State Monad의 변형
  3. Turtle Monad - Turtle (로고) 프로그램을 만들기위한 Monad. State Monad의 변형
  4. 연속 Monad - 코 루틴 모나드. 이 예제는 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
  }

계산 표현식은 모나드 체인에 대한 대체 구문을 제공합니다.

Monads와 관련된 것은 F# 계산 표현식 ( CE )입니다. 프로그래머는 일반적으로 다음 대신에 Monads를 연결하는 대체 접근법을 제공하기 위해 CE 를 구현합니다.

let v = m >>= fun x -> n >>= fun y -> return_ (x, y)

다음과 같이 작성할 수 있습니다.

let v = ce {
    let! x = m
    let! y = n
    return x, y
  }

두 스타일 모두 동등하며 선택할 개발자 우선 순위에 달려 있습니다.

CE 구현 방법을 시연하기 위해 모든 추적에서 상관 ID를 포함하는 것이 좋습니다. 이 상관 ID는 동일한 호출에 속하는 추적을 상관시키는 데 도움이됩니다. 이는 동시 호출의 추적을 포함하는 로그 파일을 가질 때 매우 유용합니다.

문제는 상관 ID를 모든 함수의 인수로 포함시키는 것이 번거롭다는 것입니다. Monads 가 암시 적 상태를 전달할 수 있으므로 Log Monad를 정의하여 로그 컨텍스트 (즉 상관 ID)를 숨길 수 있습니다.

먼저 로그 컨텍스트와 로그 컨텍스트와 함께 추적되는 함수 유형을 정의합니다.

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 ())

또한 로그 컨텍스트에서 상관 ID로 기록 할 두 개의 추적 기능을 정의합니다.

let trace v   : Function<_> = fun ctx -> printfn "CorrelationId: %A - %A" ctx.CorrelationId v
let tracef fmt              = kprintf trace fmt

trace 는 호출 될 때 로그 컨텍스트를 전달한다는 것을 의미하는 Function<unit> 입니다. 로그 컨텍스트에서 상관 ID를 가져 와서 v 와 함께 추적합니다.

또한 bindreturn_ 을 정의하고 Monad 법칙에 따라 Log Monad를 구성합니다.

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
  }

우리는 다음과 같이 g를 실행합니다.

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


Modified text is an extract of the original Stack Overflow Documentation
아래 라이선스 CC BY-SA 3.0
와 제휴하지 않음 Stack Overflow