계산 표현식
F#의 계산 식은 제어 흐름 구문 및 바인딩을 사용하여 시퀀싱 및 결합할 수 있는 계산을 작성하기 위한 편리한 구문을 제공합니다. 계산 식의 종류에 따라 모나드, 모노이드, 모나드 변환자 및 응용 함수자를 표현하는 방법으로 간주할 수 있습니다. 그러나 다른 언어(예: 하스켈의 표기법)와 달리 단일 추상화에 연결되지 않으며, 편리하고 상황에 맞는 구문을 수행하기 위해 매크로 또는 다른 형태의 메타프로그래밍에 의존하지 않습니다.
개요
계산은 여러 가지 형태를 취할 수 있습니다. 가장 일반적인 계산 형태는 쉽게 이해하고 수정할 수 있는 단일 스레드 실행입니다. 그러나 모든 형태의 계산이 단일 스레드 실행만큼 간단하지는 않습니다. 몇 가지 예는 다음과 같습니다.
- 비결정적 계산
- 비동기 계산
- 효과 있는 계산
- 생성적 계산
더 일반적으로, 애플리케이션의 특정 부분에서 수행해야 하는 상황에 맞는 연산이 있습니다. 지정된 컨텍스트 외부로 계산이 "누수"되는 것을 방지하는 추상화가 없으면, 컨텍스트에 민감한 코드를 작성하는 것이 어려울 수 있습니다. 이러한 추상화는 종종 직접 작성하기 어렵기 때문에 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)
를 정의하여 튜플링 노드의 수를 줄일 수 있고, 튜플링 노드 없이 계산 식 결과를 효율적으로 바인딩하려면 BindN(x1, x2 ..., xN, f)
또는 BindNReturn(x1, x2, ..., xN, f)
를 정의할 수 있습니다.
do!
do!
키워드는 unit
유사 형식(작성기에서 Zero
멤버에 의해 정의됨)을 반환하는 계산 식을 호출하기 위한 것입니다.
let doThingsAsync data url =
async {
do! submitData data url
...
}
비동기 워크플로의 유형은 Async<unit>
입니다. 다른 계산 표현식의 경우 형식은 CExpType<unit>
일 가능성이 있습니다.
do!
는 작성기 형식의 Bind(x, f)
멤버에 의해 정의되며, 이 조건에서 f
가 unit
를 생성합니다.
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(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
는 래핑할 항목입니다.
let! ... return
사용의 경우 성능 향상을 위해 BindReturn(x, f)
사용할 수 있습니다.
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...then 식의 빈 else 분기에 대해 호출됩니다. |
Quote |
Quotations.Expr<'T> -> Quotations.Expr<'T> |
계산 식이 Run 멤버에 인용으로 전달됨을 나타냅니다. 계산의 모든 경우를 인용 형태로 변환합니다. |
작성기 클래스의 많은 메서드는 M<'T>
구문을 사용하고 반환합니다. 이는 일반적으로 별도로 정의된 형식으로, 결합되는 계산의 종류를 특징짓는 것입니다. 예를 들어, 비동기 식에 대한 Async<'T>
과 시퀀스 워크플로우에 대한 Seq<'T>
가 있습니다. 이러한 메서드의 시그니처를 사용하여 서로 결합하고 중첩할 수 있으므로 한 구문에서 반환된 워크플로 개체를 다음 구문으로 전달할 수 있습니다.
많은 함수는 Run
, While
, TryWith
, TryFinally
및 Combine
인수로 Delay
결과를 사용합니다.
Delayed<'T>
형식은 Delay
반환 형식이므로 이러한 함수에 대한 매개 변수입니다.
Delayed<'T>
M<'T>
관련될 필요가 없는 임의의 형식일 수 있습니다. 일반적으로 M<'T>
또는 (unit -> M<'T>)
사용됩니다. 기본 구현은 M<'T>
. 보다 더 자세한 내용을 보려면 을 여기을 참조하세요.
컴파일러는 계산 식을 구문 분석할 때 이전 테이블의 메서드와 계산 식의 코드를 사용하여 일련의 중첩 함수 호출로 식을 변환합니다. 중첩 식은 다음과 같은 형식입니다.
builder.Run(builder.Delay(fun () -> {{ cexpr }}))
위의 코드에서 계산 식 작성기 클래스에 정의되지 않은 경우 Run
및 Delay
대한 호출은 생략됩니다. 여기서 {{ cexpr }}
표시된 계산 식의 본문은 작성기 클래스의 메서드에 대한 추가 호출로 변환됩니다. 이 프로세스는 다음 표의 번역에 따라 재귀적으로 정의됩니다. 중괄호 {{ ... }}
안의 코드는 아직 번역되지 않았으며, expr
은 F# 식을 나타내고 cexpr
는 계산식을 나타냅니다.
표현 | 번역 |
---|---|
{{ 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 enumerable-expr do cexpr }} |
builder.For(enumerable-expr, (fun pattern -> {{ cexpr }})) |
{{ for identifier = expr1 to expr2 do cexpr }} |
builder.For([expr1..expr2], (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
적용합니다. 이 특성은 문자열을 인수로 사용합니다. 이 이름은 사용자 지정 작업에 사용할 이름입니다. 이 이름은 계산 식의 여는 중괄호가 시작될 때 범위에 들어옵니다. 따라서 이 블록의 사용자 지정 작업과 이름이 같은 식별자를 사용하면 안 됩니다. 예를 들어 쿼리 식에서 all
또는 last
같은 식별자를 사용하지 않도록 합니다.
새 사용자 정의 작업으로 기존 빌더 확장
이미 작성기 클래스가 있는 경우 이 작성기 클래스 외부에서 사용자 지정 작업을 확장할 수 있습니다. 모듈에서 확장을 선언해야 합니다. 네임스페이스는 동일한 파일과 형식이 정의된 동일한 네임스페이스 선언 그룹을 제외하고 확장명 멤버를 포함할 수 없습니다.
다음 예제에서는 기존 FSharp.Linq.QueryBuilder
클래스의 확장을 보여 줍니다.
open System
open FSharp.Linq
type QueryBuilder with
[<CustomOperation>]
member _.any (source: QuerySource<'T, 'Q>, predicate) =
System.Linq.Enumerable.Any (source.Source, Func<_,_>(predicate))
[<CustomOperation("singleSafe")>] // you can specify your own operation name in the constructor
member _.singleOrDefault (source: QuerySource<'T, 'Q>, predicate) =
System.Linq.Enumerable.SingleOrDefault (source.Source, Func<_,_>(predicate))
사용자 지정 작업을 오버로드할 수 있습니다. 자세한 내용은 F# RFC FS-1056 -계산 식에서 사용자 지정 키워드의 오버로드 허용을 참조하세요.
계산 식을 효율적으로 컴파일
실행을 일시 중단하는 F# 계산 식은 신중하게 사용되는 저수준 기능인 다시 시작할 수 있는 코드 를 통해 매우 효율적인 상태 머신으로 컴파일할 수 있습니다. 다시 시작할 수 있는 코드는 F# RFC FS-1087에 문서화되어 있으며 태스크 식에 사용됩니다.
동기식인 F# 계산 식(즉, 실행을 일시 중단하지 않음)은
목록 식, 배열 식 및 시퀀스 식은 고성능 코드 생성을 보장하기 위해 F# 컴파일러에서 특별히 처리됩니다.
참고 항목
.NET