계산 식

F#의 계산 식은 제어 흐름 구문 및 바인딩을 사용하여 시퀀싱 및 결합할 수 있는 계산을 작성하는 편리한 구문을 제공합니다. 계산 식의 종류에 따라 모나드, 모노이드, 모나드 변압기 및 적용 균등을 표현하는 방법으로 간주할 수 있습니다. 그러나 다른 언어(예: Haskell의 표기법 )와 달리 단일 추상화에 연결되지 않으며, 편리하고 상황에 맞는 구문을 수행하기 위해 매크로 또는 다른 형태의 메타프로그래밍에 의존하지 않습니다.

개요

계산은 여러 가지 형태를 취할 수 있습니다. 가장 일반적인 계산 형식은 이해하기 쉽고 수정할 수 있는 단일 스레드 실행입니다. 그러나 모든 형태의 계산이 단일 스레드 실행만큼 간단하지는 않습니다. 일부 사례:

  • 비결정적 계산
  • 비동기 계산
  • 효과 있는 계산
  • 생성 계산

일반적으로 애플리케이션의 특정 부분에서 수행해야 하는 컨텍스트에 민감한 계산이 있습니다. 추상화 없이 지정된 컨텍스트 외부에서 계산을 "누수"하기 쉽기 때문에 상황에 맞는 코드를 작성하는 것은 어려울 수 있습니다. 이러한 추상화는 종종 직접 작성하기 어렵기 때문에 F#에는 계산 식이라고 하는 일반화된 방법이 있습니다.

계산 식은 컨텍스트에 민감한 계산을 인코딩하기 위한 균일한 구문 및 추상화 모델을 제공합니다.

모든 계산 식은 작성 기 유형에 의해 지원됩니다. 작성기 유형은 계산 식에 사용할 수 있는 작업을 정의합니다. 사용자 지정 계산 식을 만드는 방법을 보여주는 새 유형의 계산 식 만들기를 참조하세요.

구문 개요

모든 계산 식의 형식은 다음과 같습니다.

builder-expr { cexper }

이 양식 builder-expr 에서는 계산 식을 정의하는 작성기 형식의 이름이며 cexper 계산 식의 식 본문입니다. 예를 들어 async 계산 식 코드는 다음과 같습니다.

let fetchAndDownload url =
    async {
        let! data = downloadData url

        let processedData = processData data

        return processedData
    }

이전 예제와 같이 계산 식 내에서 사용할 수 있는 특수한 추가 구문이 있습니다. 계산 식에서 사용할 수 있는 식 형식은 다음과 같습니다.

expr { let! ... }
expr { and! ... }
expr { do! ... }
expr { yield ... }
expr { yield! ... }
expr { return ... }
expr { return! ... }
expr { match! ... }

이러한 각 키워드 및 기타 표준 F# 키워드는 백업 작성기 유형에 정의된 경우에만 계산 식에서 사용할 수 있습니다. 이에 대한 유일한 예외는 match!그 자체로 결과에 패턴 일치를 사용하기 let! 위한 구문 설탕입니다.

작성기 형식은 계산 식의 조각이 결합되는 방식을 제어하는 특수 메서드를 정의하는 개체입니다. 즉, 해당 메서드는 계산 식의 동작 방식을 제어합니다. 작성기 클래스를 설명하는 또 다른 방법은 루프 및 바인딩과 같은 많은 F# 구문의 작업을 사용자 지정할 수 있도록 하는 것입니다.

let!

키워드는 let! 다른 계산 식에 대한 호출 결과를 이름에 바인딩합니다.

let doThingsAsync url =
    async {
        let! data = getDataAsync url
        ...
    }

계산 식 let에 호출을 바인딩하면 계산 식의 결과가 표시되지 않습니다. 대신, 해당 계산 식에 대해 실현되지 않은 호출의 값을 바인딩합니다. 결과에 바인딩하는 데 사용합니다 let! .

let! 는 작성기 형식의 Bind(x, f) 멤버에 의해 정의됩니다.

and!

and! 워드를 사용하면 여러 계산 식 호출의 결과를 수행 방식으로 바인딩할 수 있습니다.

let doThingsAsync url =
    async {
        let! data = getDataAsync url
        and! moreData = getMoreDataAsync anotherUrl
        and! evenMoreData = getEvenMoreDataAsync someUrl
        ...
    }

고가의 let! ... let! ... 바인딩을 강제로 다시 실행하여 수많은 계산 식의 결과를 바인딩할 때 사용해야 let! ... and! ... 합니다.

and! 는 주로 작성기 형식의 MergeSources(x1, x2) 멤버에 의해 정의됩니다.

필요에 따라MergeSourcesN(x1, x2 ..., xN), 중복 제거 노드 수를 줄이기 위해 정의하거나BindNReturn(x1, x2, ..., xN, f), 노드BindN(x1, x2 ..., xN, f)를 분리하지 않고 계산 식 결과를 효율적으로 바인딩하도록 정의할 수 있습니다.

do!

키워드는 do! 작성기에서 멤버가 정의한 -like 형식을 반환 unit하는 계산 식을 호출하기 Zero 위한 것입니다.

let doThingsAsync data url =
    async {
        do! submitData data url
        ...
    }

비동기 워크플로의 경우 이 형식은 .입니다Async<unit>. 다른 계산 식의 경우 형식은 다음과 같습니다 CExpType<unit>.

do!은 작성기 형식의 Bind(x, f) 멤버에 의해 정의됩니다. unit여기서 f .

yield

yield 키워드는 계산 식에서 값을 반환하여 다음으로 IEnumerable<T>사용할 수 있도록 합니다.

let squares =
    seq {
        for i in 1..10 do
            yield i * i
    }

for sq in squares do
    printfn $"%d{sq}"

대부분의 경우 호출자가 생략할 수 있습니다. 생략 yield 하는 가장 일반적인 방법은 연산자를 사용하는 것입니다 -> .

let squares =
    seq {
        for i in 1..10 -> i * i
    }

for sq in squares do
    printfn $"%d{sq}"

다양한 값을 생성할 수 있고 조건부로 생성될 수 있는 더 복잡한 식의 경우 키워드를 생략하면 다음과 같은 작업을 수행할 수 있습니다.

let weekdays includeWeekend =
    seq {
        "Monday"
        "Tuesday"
        "Wednesday"
        "Thursday"
        "Friday"
        if includeWeekend then
            "Saturday"
            "Sunday"
    }

C#의 yield 키워드와 마찬가지로 계산 식의 각 요소는 반복될 때 다시 생성됩니다.

yield 는 작성기 형식의 Yield(x) 멤버에 의해 정의되며, 여기서 x 다시 생성할 항목입니다.

yield!

yield! 이 키워드는 계산 식에서 값 컬렉션을 평면화하기 위한 것입니다.

let squares =
    seq {
        for i in 1..3 -> i * i
    }

let cubes =
    seq {
        for i in 1..3 -> i * i * i
    }

let squaresAndCubes =
    seq {
        yield! squares
        yield! cubes
    }

printfn $"{squaresAndCubes}"  // Prints - 1; 4; 9; 1; 8; 27

계산 시 호출 yield! 된 계산 식은 항목을 하나씩 다시 생성하여 결과를 평면화합니다.

yield! 는 작성기 형식의 YieldFrom(x) 멤버에 의해 정의되며, 여기서 x 값 컬렉션입니다.

달리 yield명시적으로 yield! 지정해야 합니다. 해당 동작은 계산 식에서 암시적이지 않습니다.

return

키워드는 return 계산 식에 해당하는 형식의 값을 래핑합니다. 계산 식을 사용하는 yield것 외에도 계산 식을 "완료"하는 데 사용됩니다.

let req = // 'req' is of type 'Async<data>'
    async {
        let! data = fetch url
        return data
    }

// 'result' is of type 'data'
let result = Async.RunSynchronously req

return 는 작성기 형식의 Return(x) 멤버에 의해 정의되며, 여기서 x 래핑할 항목입니다. 사용의 BindReturn(x, f) 경우 let! ... return 성능 향상에 사용할 수 있습니다.

return!

키워드는 return! 계산 식의 값을 실현하고 계산 식에 해당하는 형식을 생성하는 래핑합니다.

let req = // 'req' is of type 'Async<data>'
    async {
        return! fetch url
    }

// 'result' is of type 'data'
let result = Async.RunSynchronously req

return! 는 작성기 형식의 ReturnFrom(x) 멤버에 의해 정의됩니다. 여기서 x 다른 계산 식이 있습니다.

match!

match! 워드를 사용하면 다른 계산 식에 대한 호출을 인라인화하고 결과에 대한 패턴 일치를 인라인할 수 있습니다.

let doThingsAsync url =
    async {
        match! callService url with
        | Some data -> ...
        | None -> ...
    }

계산 식을 match!사용하여 호출할 때는 다음과 같은 let!호출의 결과를 실현합니다. 이는 결과가 선택 사항인 계산 식을 호출할 때 자주 사용됩니다.

기본 제공 계산 식

F# 코어 라이브러리는 시퀀스 식, 비동기 식, 작업 식 및 쿼리 식의 네 가지 기본 제공 계산 식을 정의 합니다.

새 유형의 계산 식 만들기

작성기 클래스를 만들고 클래스에서 특정 특수 메서드를 정의하여 고유한 계산 식의 특성을 정의할 수 있습니다. 작성기 클래스는 필요에 따라 다음 표에 나열된 대로 메서드를 정의할 수 있습니다.

다음 표에서는 워크플로 작성기 클래스에서 사용할 수 있는 메서드에 대해 설명합니다.

방법 일반적인 서명 설명
Bind M<'T> * ('T -> M<'U>) -> M<'U> 계산 식에서 let!do! 호출됩니다.
BindN (M<'T1> * M<'T2> * ... * M<'TN> * ('T1 * 'T2 ... * 'TN -> M<'U>)) -> M<'U> 입력을 병합하지 않고 효율 let! 적이고 and! 계산 식에서 호출됩니다.

예: Bind3. Bind4
Delay (unit -> M<'T>) -> Delayed<'T> 계산 식을 함수로 래핑합니다. Delayed<'T> 는 일반적으로 M<'T> 모든 형식이거나 unit -> M<'T> 사용될 수 있습니다. 기본 구현 M<'T>은 .
Return 'T -> M<'T> return 계산 식에서 호출됩니다.
ReturnFrom M<'T> -> M<'T> return! 계산 식에서 호출됩니다.
BindReturn (M<'T1> * ('T1 -> 'T2)) -> M<'T2> 계산 식의 효율성을 let! ... return 위해 호출됩니다.
BindNReturn (M<'T1> * M<'T2> * ... * M<'TN> * ('T1 * 'T2 ... * 'TN -> M<'U>)) -> M<'U> 입력을 병합하지 않고 계산 식에서 효율적으로 let! ... and! ... return 호출됩니다.

예: Bind3Return. Bind4Return
MergeSources (M<'T1> * M<'T2>) -> M<'T1 * 'T2> and! 계산 식에서 호출됩니다.
MergeSourcesN (M<'T1> * M<'T2> * ... * M<'TN>) -> M<'T1 * 'T2 * ... * 'TN> and! 계산 식에서 호출되지만, 중복 제거 노드 수를 줄여 효율성을 향상시킵니다.

예: MergeSources3. MergeSources4
Run Delayed<'T> -> M<'T> 또는

M<'T> -> 'T
계산 식을 실행합니다.
Combine M<'T> * Delayed<'T> -> M<'T> 또는

M<unit> * M<'T> -> M<'T>
계산 식에서 시퀀싱을 위해 호출됩니다.
For seq<'T> * ('T -> M<'U>) -> M<'U> 또는

seq<'T> * ('T -> M<'U>) -> seq<M<'U>>
계산 식의 식에 대해 for...do 호출됩니다.
TryFinally Delayed<'T> * (unit -> unit) -> M<'T> 계산 식의 식에 대해 try...finally 호출됩니다.
TryWith Delayed<'T> * (exn -> M<'T>) -> M<'T> 계산 식의 식에 대해 try...with 호출됩니다.
Using 'T * ('T -> M<'U>) -> M<'U> when 'T :> IDisposable 계산 식의 바인딩에 대해 use 호출됩니다.
While (unit -> bool) * Delayed<'T> -> M<'T> 또는

(unit -> bool) * Delayed<unit> -> M<unit>
계산 식의 식에 대해 while...do 호출됩니다.
Yield 'T -> M<'T> 계산 식의 식에 대해 yield 호출됩니다.
YieldFrom M<'T> -> M<'T> 계산 식의 식에 대해 yield! 호출됩니다.
Zero unit -> M<'T> 계산 식에서 식의 if...thenelse 분기를 호출합니다.
Quote Quotations.Expr<'T> -> Quotations.Expr<'T> 계산 식이 멤버에 Run 따옴표로 전달됨을 나타냅니다. 계산의 모든 인스턴스를 따옴표로 변환합니다.

작성기 클래스의 많은 메서드는 일반적으로 비동기 식 Seq<'T> 및 시퀀스 워크플로와 같이 Async<'T> 결합되는 계산의 종류를 특징짓는 별도로 정의된 형식인 구문을 사용하고 반환 M<'T> 합니다. 이러한 메서드의 시그니처를 사용하여 서로 결합하고 중첩할 수 있으므로 한 구문에서 반환된 워크플로 개체를 다음 구문으로 전달할 수 있습니다.

많은 함수는 결과를 Delay 인수CombineRunWhileTryWithTryFinally로 사용합니다. 형식은 Delayed<'T> 반환 형식 Delay 이며, 따라서 이러한 함수에 대한 매개 변수입니다. Delayed<'T> 는 관련될 필요가 없는 임의의 형식일 수 M<'T>있습니다. 일반적으로 M<'T> 사용되거나 (unit -> M<'T>) 사용됩니다. 기본 구현은 .입니다 M<'T>. 자세한 내용은 여기 를 참조하세요.

컴파일러는 계산 식을 구문 분석할 때 이전 테이블의 메서드와 계산 식의 코드를 사용하여 일련의 중첩 함수 호출로 식을 변환합니다. 중첩 식은 다음과 같은 형식입니다.

builder.Run(builder.Delay(fun () -> {| cexpr |}))

위의 코드에서 호출 RunDelay 은 계산 식 작성기 클래스에 정의되지 않은 경우 생략됩니다. 여기에 표시된 계산 식의 본문은 다음 표에 설명된 {| cexpr |}번역에 의해 작성기 클래스의 메서드와 관련된 호출로 변환됩니다. 계산 식 {| cexpr |} 은 F# 식이고 cexpr 계산 식인 이러한 번역 expr 에 따라 재귀적으로 정의됩니다.

Translation
{ let binding in cexpr } let binding in {| cexpr |}
{ let! pattern = expr in cexpr } builder.Bind(expr, (fun pattern -> {| cexpr |}))
{ do! expr in cexpr } builder.Bind(expr, (fun () -> {| cexpr |}))
{ yield expr } builder.Yield(expr)
{ yield! expr } builder.YieldFrom(expr)
{ return expr } builder.Return(expr)
{ return! expr } builder.ReturnFrom(expr)
{ use pattern = expr in cexpr } builder.Using(expr, (fun pattern -> {| cexpr |}))
{ use! value = expr in cexpr } builder.Bind(expr, (fun value -> builder.Using(value, (fun value -> { cexpr }))))
{ if expr then cexpr0 |} if expr then { cexpr0 } else builder.Zero()
{ if expr then cexpr0 else cexpr1 |} if expr then { cexpr0 } else { cexpr1 }
{ match expr with | pattern_i -> cexpr_i } match expr with | pattern_i -> { cexpr_i }
{ for pattern in expr do cexpr } builder.For(enumeration, (fun pattern -> { cexpr }))
{ for identifier = expr1 to expr2 do cexpr } builder.For(enumeration, (fun identifier -> { cexpr }))
{ while expr do cexpr } builder.While(fun () -> expr, builder.Delay({ cexpr }))
{ try cexpr with | pattern_i -> expr_i } builder.TryWith(builder.Delay({ cexpr }), (fun value -> match value with | pattern_i -> expr_i | exn -> System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(exn).Throw())))
{ try cexpr finally expr } builder.TryFinally(builder.Delay( { cexpr }), (fun () -> expr))
{ cexpr1; cexpr2 } builder.Combine({ cexpr1 }, { cexpr2 })
{ other-expr; cexpr } expr; { cexpr }
{ other-expr } expr; builder.Zero()

이전 표 other-expr 에서 테이블에 나열되지 않은 식에 대해 설명합니다. 작성기 클래스는 모든 메서드를 구현할 필요가 없으며 이전 표에 나열된 모든 번역을 지원합니다. 구현되지 않은 구문은 해당 형식의 계산 식에서 사용할 수 없습니다. 예를 들어 계산 식에서 키워드를 use 지원하지 않으려면 작성기 클래스의 Use 정의를 생략할 수 있습니다.

다음 코드 예제에서는 계산을 한 번에 한 단계씩 평가할 수 있는 일련의 단계로 캡슐화하는 계산 식을 보여 줍니다. 구분된 공용 구조체 형식 OkOrException은 지금까지 평가된 대로 식의 오류 상태를 인코딩합니다. 이 코드는 일부 작성기 메서드의 상용구 구현과 같이 계산 식에서 사용할 수 있는 몇 가지 일반적인 패턴을 보여 줍니다.

/// Represents computations that can be run step by step
type Eventually<'T> =
    | Done of 'T
    | NotYetDone of (unit -> Eventually<'T>)

module Eventually =

    /// Bind a computation using 'func'.
    let rec bind func expr =
        match expr with
        | Done value -> func value
        | NotYetDone work -> NotYetDone (fun () -> bind func (work()))

    /// Return the final value
    let result value = Done value

    /// The catch for the computations. Stitch try/with throughout
    /// the computation, and return the overall result as an OkOrException.
    let rec catch expr =
        match expr with
        | Done value -> result (Ok value)
        | NotYetDone work ->
            NotYetDone (fun () ->
                let res = try Ok(work()) with | exn -> Error exn
                match res with
                | Ok cont -> catch cont // note, a tailcall
                | Error exn -> result (Error exn))

    /// The delay operator.
    let delay func = NotYetDone (fun () -> func())

    /// The stepping action for the computations.
    let step expr =
        match expr with
        | Done _ -> expr
        | NotYetDone func -> func ()

    /// The tryFinally operator.
    /// This is boilerplate in terms of "result", "catch", and "bind".
    let tryFinally expr compensation =
        catch (expr)
        |> bind (fun res ->
            compensation();
            match res with
            | Ok value -> result value
            | Error exn -> raise exn)

    /// The tryWith operator.
    /// This is boilerplate in terms of "result", "catch", and "bind".
    let tryWith exn handler =
        catch exn
        |> bind (function Ok value -> result value | Error exn -> handler exn)

    /// The whileLoop operator.
    /// This is boilerplate in terms of "result" and "bind".
    let rec whileLoop pred body =
        if pred() then body |> bind (fun _ -> whileLoop pred body)
        else result ()

    /// The sequential composition operator.
    /// This is boilerplate in terms of "result" and "bind".
    let combine expr1 expr2 =
        expr1 |> bind (fun () -> expr2)

    /// The using operator.
    /// This is boilerplate in terms of "tryFinally" and "Dispose".
    let using (resource: #System.IDisposable) func =
        tryFinally (func resource) (fun () -> resource.Dispose())

    /// The forLoop operator.
    /// This is boilerplate in terms of "catch", "result", and "bind".
    let forLoop (collection:seq<_>) func =
        let ie = collection.GetEnumerator()
        tryFinally
            (whileLoop
                (fun () -> ie.MoveNext())
                (delay (fun () -> let value = ie.Current in func value)))
            (fun () -> ie.Dispose())

/// The builder class.
type EventuallyBuilder() =
    member x.Bind(comp, func) = Eventually.bind func comp
    member x.Return(value) = Eventually.result value
    member x.ReturnFrom(value) = value
    member x.Combine(expr1, expr2) = Eventually.combine expr1 expr2
    member x.Delay(func) = Eventually.delay func
    member x.Zero() = Eventually.result ()
    member x.TryWith(expr, handler) = Eventually.tryWith expr handler
    member x.TryFinally(expr, compensation) = Eventually.tryFinally expr compensation
    member x.For(coll:seq<_>, func) = Eventually.forLoop coll func
    member x.Using(resource, expr) = Eventually.using resource expr

let eventually = new EventuallyBuilder()

let comp =
    eventually {
        for x in 1..2 do
            printfn $" x = %d{x}"
        return 3 + 4
    }

/// Try the remaining lines in F# interactive to see how this
/// computation expression works in practice.
let step x = Eventually.step x

// returns "NotYetDone <closure>"
comp |> step

// prints "x = 1"
// returns "NotYetDone <closure>"
comp |> step |> step

// prints "x = 1"
// prints "x = 2"
// returns "Done 7"
comp |> step |> step |> step |> step

계산 식에는 식이 반환하는 기본 형식이 있습니다. 기본 형식은 계산된 결과 또는 수행할 수 있는 지연된 계산을 나타내거나 일부 유형의 컬렉션을 반복하는 방법을 제공할 수 있습니다. 이전 예제에서 기본 형식은 .입니다 Eventually<_>. 시퀀스 식의 경우 기본 형식은 .입니다 System.Collections.Generic.IEnumerable<T>. 쿼리 식의 경우 기본 형식은 .입니다 System.Linq.IQueryable. 비동기 식의 경우 기본 형식은 .입니다 Async. 개체는 Async 결과를 계산하기 위해 수행할 작업을 나타냅니다. 예를 들어 계산을 실행하고 결과를 반환하도록 호출 Async.RunSynchronously 합니다.

사용자 지정 작업

계산 식에서 사용자 지정 연산을 정의하고 계산 식에서 사용자 지정 연산을 연산자로 사용할 수 있습니다. 예를 들어 쿼리 식에 쿼리 연산자를 포함할 수 있습니다. 사용자 지정 연산을 정의할 때 계산 식에서 Yield 및 For 메서드를 정의해야 합니다. 사용자 지정 작업을 정의하려면 계산 식에 대한 작성기 클래스에 배치한 다음 CustomOperationAttribute. 이 특성은 문자열을 인수로 사용합니다. 이 이름은 사용자 지정 작업에 사용할 이름입니다. 이 이름은 계산 식의 중괄호 열기 시작 시 범위에 들어옵니다. 따라서 이 블록에서 사용자 지정 작업과 이름이 같은 식별자를 사용하면 안 됩니다. 예를 들어 쿼리 식과 같은 alllast 식별자를 사용하지 않도록 합니다.

새 사용자 지정 작업을 사용하여 기존 작성기 확장

작성기 클래스가 이미 있는 경우 이 작성기 클래스 외부에서 사용자 지정 작업을 확장할 수 있습니다. 모듈에서 확장을 선언해야 합니다. 네임스페이스는 동일한 파일 및 형식이 정의된 동일한 네임스페이스 선언 그룹을 제외하고 확장명 멤버를 포함할 수 없습니다.

다음 예제에서는 기존 FSharp.Linq.QueryBuilder 클래스의 확장을 보여줍니다.

open System
open FSharp.Linq

type QueryBuilder with

    [<CustomOperation("existsNot")>]
    member _.ExistsNot (source: QuerySource<'T, 'Q>, predicate) =
        System.Linq.Enumerable.Any (source.Source, Func<_,_>(predicate)) |> not

사용자 지정 작업을 오버로드할 수 있습니다. 자세한 내용은 F# RFC FS-1056 - 계산 식에서 사용자 지정 키워드의 오버로드 허용을 참조하세요.

계산 식을 효율적으로 컴파일

실행을 일시 중단하는 F# 계산 식은 다시 시작 가능한 코드라는 하위 수준 기능을 신중하게 사용하여 매우 효율적인 상태 컴퓨터로 컴파일할 수 있습니다. 다시 시작 가능한 코드는 F# RFC FS-1087 에 설명되어 있으며 작업 식에 사용됩니다.

동기식인 F# 계산 식(즉, 실행을 일시 중단하지 않음)은 특성을 포함한 InlineIfLambda인라인 함수를 사용하여 효율적인 상태 컴퓨터로 컴파일할 수도 있습니다. 예제는 F# RFC FS-1098에서 제공됩니다.

목록 식, 배열 식 및 시퀀스 식은 고성능 코드 생성을 보장하기 위해 F# 컴파일러에서 특별히 처리됩니다.

참고 항목