정적으로 결정되는 타입 매개변수
정적으로 결정되는 타입 매개변수 (Statically Resolved Type Parameters)
정적으로 결정되는 타입 매개변수는 **컴파일 시점(compile time)**에 실제 타입으로 치환되는 타입 매개변수예요. 일반적인 제네릭 타입 매개변수가 실행 시점(run time)에 처리되는 것과는 달라요. F#에서 인라인 함수와 함께 쓰이는 이 특별한 타입 파라미터의 문법과 쓰임새를 살펴볼게요.
본문
구문 (Syntax)
'type-parameter
F# 7.0 이전 버전까지는 다음과 같은 구문을 사용해야 했어요.
^type-parameter
설명 (Remarks)
F#에는 서로 다른 두 종류의 타입 매개변수가 있어요. 첫 번째는 표준 제네릭 타입 매개변수인데, 다른 .NET 언어에서 쓰는 제네릭 타입 매개변수와 같아요. 다른 하나가 바로 정적으로 결정되는 타입 매개변수로, 인라인(inline) 함수에서만 사용할 수 있어요.
정적으로 결정되는 타입 매개변수는 주로 **멤버 제약(member constraints)**과 함께 쓰일 때 유용해요. 멤버 제약은 타입 인자가 특정 멤버를 하나 이상 가져야 한다고 지정할 수 있게 해 주는 제약이에요. 이런 종류의 제약은 일반 제네릭 타입 매개변수로는 만들 수 없어요.
아래 표는 두 종류의 타입 매개변수가 얼마나 비슷하고 다른지를 정리한 거예요.
| 기능 | 제네릭 | 정적으로 결정되는 |
|---|---|---|
| 해결 시점 | 실행 시점 | 컴파일 시점 |
| 멤버 제약 | 멤버 제약과 함께 사용할 수 없어요. | 멤버 제약과 함께 사용할 수 있어요. |
| 코드 생성 | 표준 제네릭 타입 매개변수를 가진 타입(또는 메서드)은 제네릭 타입이나 메서드 하나를 생성해요. | 필요한 타입마다 타입과 메서드의 인스턴스화(instantiation)가 여러 개 생성돼요. |
| 타입과의 사용 | 타입에 사용할 수 있어요. | 타입에는 사용할 수 없어요. |
| 인라인 함수와의 사용 | 인라인 함수는 표준 제네릭 타입 매개변수로는 매개변수화할 수 없어요. 입력이 완전히 제네릭이 아니면 F# 컴파일러가 이를 특수화하거나, 특수화할 방법이 없으면 오류를 내요. | 정적으로 결정되는 타입 매개변수는 인라인이 아닌 함수나 메서드에는 사용할 수 없어요. |
F# 핵심 라이브러리의 많은 함수, 특히 연산자들이 정적으로 결정되는 타입 매개변수를 사용해요. 이 함수들과 연산자는 인라인이라서, 숫자 계산에 대해 효율적인 코드 생성을 만들어 내요.
연산자를 사용하거나, 정적으로 결정되는 타입 매개변수를 가진 다른 함수를 사용하는 인라인 메서드와 함수도 자신이 정적으로 결정되는 타입 매개변수를 가질 수 있어요. 이때 타입 추론이 종종 그런 인라인 함수가 정적으로 결정되는 타입 매개변수를 가진다고 추론하기도 해요. 다음 예시는 정적으로 결정되는 타입 매개변수를 가진 것으로 추론되는 연산자 정의를 보여줘요.
let inline (+@) x y = x + x * y
// Call that uses int.
printfn "%d" (1 +@ 1)
// Call that uses float.
printfn "%f" (1.0 +@ 0.5)
(+@)가 결정되는 타입은 (+)와 (*) 둘 모두를 사용한 결과에 기반해요. 두 연산자가 모두 정적으로 결정되는 타입 매개변수에 멤버 제약을 추론하게 만들기 때문이에요. F# 인터프리터에서 보여 주는 결정된 타입은 아래와 같아요.
'a -> 'c -> 'd
when ('a or 'b) : (static member ( + ) : 'a * 'b -> 'd) and
('a or 'c) : (static member ( * ) : 'a * 'c -> 'b)
출력은 아래와 같아요.
2
1.500000
다음 예시는 SRTP를 인스턴스 메서드와 정적 메서드에서 사용하는 방법을 보여줘요.
type Record =
{ Number: int }
member this.Double() = { Number = this.Number * 2 }
static member Zero() = { Number = 0 }
let inline double<'a when 'a:(member Double: unit -> 'a)> (x: 'a) = x.Double()
let inline zero<'a when 'a:(static member Zero: unit -> 'a)> () = 'a.Zero()
let r: Record = zero ()
let doubleR = double r
F# 7.0부터는 아래 예시처럼 제약을 계속 반복하지 않고 'a.Zero()를 사용할 수 있어요.
F# 4.1부터는 정적으로 결정되는 타입 매개변수 시그니처에 구체적인 타입 이름도 지정할 수 있게 됐어요. 이전 버전에서는 타입 이름을 컴파일러가 추론할 수는 있어도 시그니처에 명시할 수는 없었어요. F# 4.1부터는 정적으로 결정되는 타입 매개변수 시그니처에서 구체적인 타입 이름을 지정할 수 있어요. 다음이 그 예시예요. (이 예시에서는 '로 단순화하는 게 지원되지 않으므로 여전히 ^를 사용해야 한다는 점을 기억하세요.)
let inline konst x _ = x
type CFunctor() =
static member inline fmap (f: ^a -> ^b, a: ^a list) = List.map f a
static member inline fmap (f: ^a -> ^b, a: ^a option) =
match a with
| None -> None
| Some x -> Some (f x)
// default implementation of replace
static member inline replace< ^a, ^b, ^c, ^d, ^e when ^a :> CFunctor and (^a or ^d): (static member fmap: (^b -> ^c) * ^d -> ^e) > (a, f) =
((^a or ^d) : (static member fmap : (^b -> ^c) * ^d -> ^e) (konst a, f))
// call overridden replace if present
static member inline replace< ^a, ^b, ^c when ^b: (static member replace: ^a * ^b -> ^c)>(a: ^a, f: ^b) =
(^b : (static member replace: ^a * ^b -> ^c) (a, f))
let inline replace_instance< ^a, ^b, ^c, ^d when (^a or ^c): (static member replace: ^b * ^c -> ^d)> (a: ^b, f: ^c) =
((^a or ^c): (static member replace: ^b * ^c -> ^d) (a, f))
// Note the concrete type 'CFunctor' specified in the signature
let inline replace (a: ^a) (f: ^b): ^a0 when (CFunctor or ^b): (static member replace: ^a * ^b -> ^a0) =
replace_instance<CFunctor, _, _, _> (a, f)