계산 식
계산 식 (Computation Expressions)
계산 식은 F#에서 계산을 순서대로 엮고 조합해서 쓰기 좋게 만들어 주는 문법이에요. 뭔가 막연하게 느껴질 수 있는데요, 쉽게 말하면 특정한 방식으로 실행되는 계산 단위를 만드는 틀이라고 보면 돼요. 실제로는 async { ... } 같은 코드를 써 본 적이 있다면 이미 계산 식을 사용하고 있던 거예요. 이 글에서는 계산 식이 무엇인지, 어떤 형태로 쓰이는지, 직접 새 계산 식을 만드는 법까지 차근차근 살펴볼게요.
출처: https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/computation-expressions
본문
F#의 계산 식(computation expression)은 제어 흐름 구문과 바인딩을 이용해 계산을 순서대로 엮고 조합해서 작성하기 편한 문법을 제공해요. 계산 식의 종류에 따라 모나드(monad), 모노이드(monoid), 모나드 변환기(monad transformer), 애플리케이티브 펑터(applicative functor)를 표현하는 방식이라고 생각할 수 있어요. 다만 Haskell의 do-notation 같은 다른 언어들과 달리 단일 추상화에 묶여 있지 않고, 편리하고 문맥에 맞는 문법을 만들기 위해 매크로나 다른 형태의 메타프로그래밍에 의존하지 않아요.
개요 (Overview)
계산은 다양한 형태를 가질 수 있어요. 가장 흔한 형태는 이해하고 수정하기 쉬운 단일 스레드 실행이에요. 하지만 모든 계산이 단일 스레드 실행만큼 단순하진 않은데요, 예를 들면 이런 것들이 있어요.
- 비결정적 계산 (non-deterministic computations)
- 비동기 계산 (asynchronous computations)
- 효과가 있는 계산 (effectful computations)
- 생성적 계산 (generative computations)
더 일반적으로 말하면, 애플리케이션의 특정 부분에서 수행해야 하는 문맥에 민감한 계산(context-sensitive computations)이 있어요. 문맥에 민감한 코드를 작성하는 건 까다로울 수 있는데, 이를 막아주는 추상화가 없다면 계산이 주어진 문맥 밖으로 "새어 나가는"(leak) 일이 쉽게 생기거든요. 이런 추상화를 직접 만드는 건 종종 어려운데, F#은 이를 일반화한 방법인 계산 식을 제공해요.
계산 식은 문맥에 민감한 계산을 인코딩하기 위한 통일된 문법과 추상화 모델을 제공해요.
모든 계산 식은 빌더 타입(builder type)에 기반해요. 빌더 타입은 해당 계산 식에서 사용할 수 있는 연산을 정의해요. 사용자 정의 계산 식을 만드는 방법은 새 종류의 계산 식 만들기에서 확인할 수 있어요.
문법 개요 (Syntax overview)
모든 계산 식은 다음과 같은 형태를 가져요.
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! 다음에 결과에 대한 패턴 매칭을 쓰는 것의 문법적 설탕(syntactic sugar)이에요.
빌더 타입은 계산 식의 조각들이 어떻게 결합되는지를 정하는 특별한 메서드를 정의하는 객체예요. 다시 말해, 그 메서드들이 계산 식이 어떻게 동작하는지를 제어해요. 빌더 클래스를 다른 방식으로 설명하자면, 루프나 바인딩 같은 많은 F# 구문의 동작을 사용자 지정할 수 있게 해 준다고 말할 수 있어요.
let!
let! 키워드는 다른 계산 식을 호출한 결과를 이름에 바인딩해요.
let doThingsAsync url =
async {
let! data = getDataAsync url
...
}
let으로 계산 식 호출을 바인딩하면 계산 식의 결과를 얻지 못해요. 대신, 아직 실행되지 않은 호출의 값이 바인딩될 뿐이에요. 결과에 바인딩하려면 let!을 사용해야 해요.
let!은 빌더 타입의 Bind(x, f) 멤버로 정의돼요.
and!
and! 키워드는 여러 계산 식 호출의 결과를 더 효율적으로 바인딩할 수 있게 해 줘요. 이 키워드는 표준 모나딕 방식과는 다른 계산 모델을 제공하는 애플리케이티브 계산 식(applicative computation expression)을 가능하게 해요.
let doThingsAsync url =
async {
let! data = getDataAsync url
and! moreData = getMoreDataAsync anotherUrl
and! evenMoreData = getEvenMoreDataAsync someUrl
...
}
let! ... let! ...을 연달아 쓰면 서로 독립적인 계산이라도 순차적으로 실행돼요. 반면 let! ... and! ...은 계산들이 서로 독립적이라는 뜻이라 애플리케이티브 결합(applicative combination)이 이뤄져요. 이 독립성 덕분에 계산 식 작성자는 다음과 같은 일을 할 수 있어요.
- 계산을 더 효율적으로 실행한다.
- 계산을 병렬로 실행할 수 있다.
- 불필요한 순차 의존성 없이 결과를 누적한다.
제약은 and!로 결합한 계산이 같은 let!/and! 체인 안에서 이전에 바인딩된 값의 결과에 의존할 수 없다는 거예요. 이 트레이드오프 덕분에 성능상 이점을 얻는 거죠.
and!는 주로 빌더 타입의 MergeSources(x1, x2) 멤버로 정의돼요.
선택적으로 MergeSourcesN(x1, x2 ..., xN)을 정의해 튜플링 노드(tupling node) 수를 줄일 수 있고, BindN(x1, x2 ..., xN, f) 또는 BindNReturn(x1, x2, ..., xN, f)을 정의해 튜플링 노드 없이 계산 식 결과를 효율적으로 바인딩할 수도 있어요.
애플리케이티브 계산 식에 대한 자세한 내용은 F# 5의 애플리케이티브 계산 식과 F# RFC FS-1063을 참고하세요.
do!
do! 키워드는 unit과 같은 타입(빌더의 Zero 멤버로 정의됨)을 반환하는 계산 식을 호출할 때 사용해요.
let doThingsAsync data url =
async {
do! submitData data url
...
}
async 워크플로에서는 이 타입이 Async<unit>이에요. 다른 계산 식에서는 타입이 CExpType<unit>일 가능성이 높아요.
do!는 빌더 타입의 Bind(x, f) 멤버로 정의되는데, 여기서 f는 unit을 만들어 내요.
yield
yield 키워드는 계산 식에서 값을 반환해 IEnumerable
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는 빌더 타입의 Yield(x) 멤버로 정의되는데, x는 yield되어 나올 항목이에요.
yield!
yield! 키워드는 계산 식에서 값들의 컬렉션을 펼치는(flatten) 데 사용해요.
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되어 나오면서 결과가 펼쳐져요.
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!처럼 호출 결과를 실현해요. 결과가 option인 계산 식을 호출할 때 자주 사용돼요.
내장 계산 식 (Built-in computation expressions)
F# 핵심 라이브러리는 네 가지 내장 계산 식을 정의해요. 시퀀스 식 (Sequence Expressions), async 식 (Async expressions), task 식 (Task expressions), 쿼리 식 (Query Expressions)이에요.
새 종류의 계산 식 만들기 (Creating a New Type of Computation Expression)
빌더 클래스를 만들고 그 클래스에 특정 특별 메서드를 정의하면 자신만의 계산 식 특성을 정의할 수 있어요. 빌더 클래스는 다음 표에 나열된 메서드들을 선택적으로 정의할 수 있어요.
다음 표는 워크플로 빌더 클래스에서 사용할 수 있는 메서드들을 설명해요.
| Method | Typical signature(s) | Description |
|---|---|---|
Bind |
M<'T> * ('T -> M<'U>) -> M<'U> |
Called for let! and do! in computation expressions. |
BindN |
(M<'T1> * M<'T2> * ... * M<'TN> * ('T1 * 'T2 ... * 'TN -> M<'U>)) -> M<'U> |
Called for efficient let! and and! in computation expressions without merging inputs.for example, Bind3, Bind4. |
Delay |
(unit -> M<'T>) -> Delayed<'T> |
Wraps a computation expression as a function. Delayed<'T> can be any type, commonly M<'T> or unit -> M<'T> are used. The default implementation returns a M<'T>. |
Return |
'T -> M<'T> |
Called for return in computation expressions. |
ReturnFrom |
M<'T> -> M<'T> |
Called for return! in computation expressions. |
ReturnFromFinal |
M<'T> -> M<'T> |
If present, called for return! and do! when in tail-call position. |
BindReturn |
(M<'T1> * ('T1 -> 'T2)) -> M<'T2> |
Called for an efficient let! ... return in computation expressions. |
BindNReturn |
(M<'T1> * M<'T2> * ... * M<'TN> * ('T1 * 'T2 ... * 'TN -> M<'U>)) -> M<'U> |
Called for efficient let! ... and! ... return in computation expressions without merging inputs.for example, Bind3Return, Bind4Return. |
MergeSources |
(M<'T1> * M<'T2>) -> M<'T1 * 'T2> |
Called for and! in computation expressions. |
MergeSourcesN |
(M<'T1> * M<'T2> * ... * M<'TN>) -> M<'T1 * 'T2 * ... * 'TN> |
Called for and! in computation expressions, but improves efficiency by reducing the number of tupling nodes.for example, MergeSources3, MergeSources4. |
Run |
Delayed<'T> -> M<'T> orM<'T> -> 'T |
Executes a computation expression. |
Combine |
M<'T> * Delayed<'T> -> M<'T> orM<unit> * M<'T> -> M<'T> |
Called for sequencing in computation expressions. |
For |
seq<'T> * ('T -> M<'U>) -> M<'U> orseq<'T> * ('T -> M<'U>) -> seq<M<'U>> |
Called for for...do expressions in computation expressions. |
TryFinally |
Delayed<'T> * (unit -> unit) -> M<'T> |
Called for try...finally expressions in computation expressions. |
TryWith |
Delayed<'T> * (exn -> M<'T>) -> M<'T> |
Called for try...with expressions in computation expressions. |
Using |
'T * ('T -> M<'U>) -> M<'U> when 'T :> IDisposable |
Called for use bindings in computation expressions. |
While |
(unit -> bool) * Delayed<'T> -> M<'T> or(unit -> bool) * Delayed<unit> -> M<unit> |
Called for while...do expressions in computation expressions. |
Yield |
'T -> M<'T> |
Called for yield expressions in computation expressions. |
YieldFrom |
M<'T> -> M<'T> |
Called for yield! expressions in computation expressions. |
YieldFromFinal |
M<'T> -> M<'T> |
If present, called for yield! when in tail-call position and in case of do! in tail-call position as a fallback for ReturnFromFinal |
Zero |
unit -> M<'T> |
Called for empty else branches of if...then expressions in computation expressions. |
Quote |
Quotations.Expr<'T> -> Quotations.Expr<'T> |
Indicates that the computation expression is passed to the Run member as a quotation. It translates all instances of a computation into a quotation. |
빌더 클래스의 많은 메서드는 M<'T> 구문을 사용하고 반환하는데, 이는 보통 결합되는 계산의 종류를 나타내는 별도로 정의된 타입이에요. 예를 들어 async 식에는 Async<'T>, 시퀀스 워크플로에는 Seq<'T>를 써요. 이 메서드들의 시그니처 덕분에 서로 결합되고 중첩될 수 있어서, 한 구문에서 반환된 워크플로 객체를 다음 구문에 넘길 수 있어요.
많은 함수가 Delay의 결과를 인자로 사용해요. Run, While, TryWith, TryFinally, Combine이 그렇죠. Delayed<'T> 타입은 Delay의 반환 타입이자 이 함수들의 매개변수예요. Delayed<'T>는 M<'T>와 관련이 없어도 되는 임의의 타입일 수 있는데, 흔히 M<'T>나 (unit -> M<'T>)을 사용해요. 기본 구현은 M<'T>예요. 더 자세히 알고 싶다면 타입 제약 이해하기 (Understanding the type constraints)를 참고하세요.
컴파일러는 계산 식을 파싱할 때 앞 표의 메서드들과 계산 식 안의 코드를 사용해 표현식을 일련의 중첩 함수 호출로 변환해요. 중첩 표현식은 다음과 같은 형태예요.
builder.Run(builder.Delay(fun () -> {{ cexpr }}))
위 코드에서 Run과 Delay에 대한 호출은 계산 식 빌더 클래스에 정의되어 있지 않으면 생략돼요. 여기서 {{ cexpr }}로 표시된 계산 식 본문은 빌더 클래스 메서드들에 대한 추가 호출로 변환돼요. 이 과정은 다음 표의 변환 규칙에 따라 재귀적으로 정의돼요. 이중 대괄호 {{ ... }} 안의 코드는 아직 변환될 코드를 뜻하고, expr은 F# 식, cexpr은 계산 식을 나타내요.
| Expression | 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 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 정의를 생략하면 돼요.
다음 코드 예시는 계산을 한 번에 한 단계씩 평가할 수 있는 일련의 단계로 캡슐화하는 계산 식을 보여줘요. 판별 공용체(discriminated union) 타입인 OkOrException은 지금까지 평가된 식의 오류 상태를 인코딩해요. 이 코드는 빌더 메서드의 일부 상용구(boilerplate) 구현 같은, 계산 식에서 쓸 수 있는 몇 가지 전형적인 패턴을 보여줘요.
/// 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
계산 식에는 식이 반환하는 기반 타입(underlying type)이 있어요. 기반 타입은 계산된 결과나 수행할 지연 계산을 나타낼 수도 있고, 어떤 컬렉션 타입을 반복하는 방법을 제공할 수도 있어요. 앞의 예시에서 기반 타입은 Eventually<_>였어요. 시퀀스 식의 기반 타입은 System.Collections.Generic.IEnumerableAsync예요. Async 객체는 결과를 계산하기 위해 수행해야 할 작업을 나타내요. 예를 들어 Async.RunSynchronously를 호출해 계산을 실행하고 결과를 반환할 수 있어요.
사용자 지정 연산 (Custom Operations)
계산 식에 사용자 지정 연산(custom operation)을 정의하고 계산 식 안에서 연산자로 사용할 수 있어요. 예를 들어 쿼리 식에 쿼리 연산자를 포함할 수 있죠. 사용자 지정 연산을 정의할 때는 계산 식에서 Yield와 For 메서드를 정의해야 해요. 사용자 지정 연산을 정의하려면 계산 식의 빌더 클래스에 넣은 다음 CustomOperationAttribute를 적용하면 돼요. 이 특성은 인자로 문자열을 받는데, 이 문자열이 사용자 지정 연산에서 사용될 이름이에요. 이 이름은 계산 식의 여는 중괄호가 시작되는 지점부터 스코프에 들어와요. 따라서 이 블록 안에서는 사용자 지정 연산과 같은 이름을 가진 식별자를 사용하면 안 돼요. 예를 들어 쿼리 식에서는 all이나 last 같은 식별자 사용을 피해야 해요.
기존 빌더를 새 사용자 지정 연산으로 확장하기 (Extending existing Builders with new Custom Operations)
이미 빌더 클래스가 있다면 그 사용자 지정 연산을 빌더 클래스 밖에서 확장할 수 있어요. 확장은 모듈에서 선언해야 해요. 네임스페이스는 타입이 정의된 동일한 파일과 동일한 네임스페이스 선언 그룹 안을 제외하고는 확장 멤버를 포함할 수 없어요.
다음 예시는 기존 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 - Allow overloads of custom keywords in computation expressions을 참고하세요.
계산 식 효율적으로 컴파일하기 (Compiling computation expressions efficiently)
실행을 중단(suspend)하는 F# 계산 식은 resumable code라고 하는 저수준 기능을 신중히 사용하면 고효율 상태 머신으로 컴파일될 수 있어요. Resumable code는 F# RFC FS-1087에 문서화되어 있고 Task Expressions에 사용돼요.
동기식인(즉 실행을 중단하지 않는) F# 계산 식은 대신 inline 함수와 InlineIfLambda 특성을 사용해 효율적인 상태 머신으로 컴파일될 수 있어요. 예시는 F# RFC FS-1098에 있어요.
리스트 식, 배열 식, 시퀀스 식은 F# 컴파일러가 고성능 코드 생성을 보장하도록 특별히 처리해요.