정적으로 결정되는 타입 매개변수

정적으로 결정되는 타입 매개변수 (Statically Resolved Type Parameters)

정적으로 결정되는 타입 매개변수는 **컴파일 시점(compile time)**에 실제 타입으로 치환되는 타입 매개변수예요. 일반적인 제네릭 타입 매개변수가 실행 시점(run time)에 처리되는 것과는 달라요. F#에서 인라인 함수와 함께 쓰이는 이 특별한 타입 파라미터의 문법과 쓰임새를 살펴볼게요.

출처: https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/generics/statically-resolved-type-parameters

본문

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

더 알아보기 (Learn more)