F# 형식 확장(Type Extensions)

F# 형식 확장(Type Extensions)

형식 확장(영어로는 type extension, augmentation이라고도 불러요)은 이미 정의된 객체 형식(object type)에 새 멤버를 추가할 수 있게 해 주는 기능 묶음이에요. 세 가지가 있는데요, 내장 형식 확장(intrinsic type extensions), 선택 형식 확장(optional type extensions), 그리고 **확장 메서드(extension methods)**예요.

각각 쓰이는 상황이 서로 다르고, 저마다 장단점도 달라요. 이 글에서는 세 가지를 하나씩 짚어 보면서, 언제 어떤 걸 골라야 하는지 자연스럽게 정리해 볼게요.

출처: Type Extensions - F# | Microsoft Learn

본문

먼저 세 가지 형식 확장이 사용하는 문법부터 볼게요. 큰 틀을 잡아 두면 나머지 설명을 따라가기 쉬워요.

구문(Syntax)

// Intrinsic and optional extensions
type typename with
    member self-identifier.member-name =
        body
    ...

// Extension methods
open System.Runtime.CompilerServices

[<Extension>]
type Extensions() =
    [<Extension>]
    static member extension-name (ty: typename, [args]) =
        body
    ...

내장 형식 확장과 선택 형식 확장은 type typename with ... 형태로 선언하고, 확장 메서드는 [<Extension>] 특성을 붙인 정적 멤버로 선언하는 걸 볼 수 있어요.

내장 형식 확장(Intrinsic type extensions)

내장 형식 확장은 사용자가 직접 정의한 형식을 확장하는 형식 확장이에요.

내장 형식 확장은 확장 대상이 되는 형식과 같은 파일, 그리고 같은 네임스페이스나 모듈 안에 정의되어야 해요. 조금이라도 다른 위치에 정의하면 그건 선택 형식 확장으로 취급돼요.

내장 형식 확장은 형식 선언에서 기능을 분리할 때 더 깔끔한 방법이 되기도 해요. 다음 예시에서 내장 형식 확장을 어떻게 정의하는지 볼게요.

namespace Example

type Variant =
    | Num of int
    | Str of string

module Variant =
    let print v =
        match v with
        | Num n -> printf "Num %d" n
        | Str s -> printf "Str %s" s

// Add a member to Variant as an extension
type Variant with
    member x.Print() = Variant.print x

형식 확장을 쓰면 아래 항목들을 서로 분리할 수 있어요.

  • Variant 형식의 선언
  • Variant를 그 "모양(shape)"에 따라 출력하는 기능
  • 객체 스타일의 . 표기법으로 출력 기능에 접근하는 방법

이렇게 하면 모든 걸 Variant의 멤버로 다 정의하는 것과는 다른 방식이 돼요. 원래 방식이 근본적으로 더 낫다는 건 아니지만, 어떤 상황에서는 기능을 훨씬 깔끔하게 표현해 주기도 해요.

내장 형식 확장은 확장 대상이 되는 형식의 멤버로서 컴파일되고, 리플렉션(reflection)으로 형식을 살펴보면 그 형식에 나타나요.

선택 형식 확장(Optional type extensions)

선택 형식 확장은 확장 대상 형식의 원래 모듈·네임스페이스·어셈블리 밖에 나타나는 확장이에요.

선택 형식 확장은 내가 직접 정의하지 않은 형식을 확장할 때 특히 유용해요. 예를 들어 볼게요.

module Extensions

type IEnumerable<'T> with
    /// Repeat each element of the sequence n times
    member xs.RepeatElements(n: int) =
        seq {
            for x in xs do
                for _ in 1 .. n -> x
        }

이제 작업 중인 범위에서 Extensions 모듈을 열어 두면 RepeatElements를 마치 IEnumerable의 멤버인 것처럼 쓸 수 있어요.

선택 형식 확장은 리플렉션으로 살펴볼 때 확장된 형식에 나타나지 않아요. 선택 형식 확장은 반드시 모듈 안에 있어야 하고, 그 확장이 들어 있는 모듈이 열려 있거나(open) 달리 범위 안에 있을 때만 유효해요.

선택 형식 확장 멤버는 객체 인스턴스가 첫 번째 인자로 암시적으로 전달되는 정적 멤버로 컴파일돼요. 다만 사용할 때는 선언 방식에 따라 인스턴스 멤버처럼, 또는 정적 멤버처럼 동작해요.

선택 형식 확장 멤버는 C#이나 Visual Basic 사용자에게는 보이지 않아요. 오직 다른 F# 코드에서만 쓸 수 있답니다.

내장·선택 형식 확장의 제네릭 제약(Generic limitation)

형식 변수에 제약(constraint)이 붙은 제네릭 형식에도 형식 확장을 선언할 수 있어요. 이때 요구되는 것은, 확장 선언의 제약이 선언된 형식의 제약과 일치해야 한다는 점이에요.

그런데 선언된 형식과 형식 확장 사이에 제약이 맞더라도, 확장된 멤버의 본문에 의해 추론된 제약이 선언된 형식이 요구하는 것과 다른 조건을 형식 매개변수에 부과할 수 있어요. 예를 들면 이렇죠.

open System.Collections.Generic

// NOT POSSIBLE AND FAILS TO COMPILE!
//
// The member 'Sum' has a different requirement on 'T than the type IEnumerable<'T>
type IEnumerable<'T> with
    member this.Sum() = Seq.sum this

이 코드는 선택 형식 확장으로는 절대 동작하게 만들 수 없어요.

  • 그대로 두면 Sum 멤버가 'T에 거는 제약(static member get_Zero, static member (+))이 형식 확장이 정의하는 제약과 달라요.
  • 형식 확장을 Sum과 같은 제약으로 바꾸면 이번엔 IEnumerable<'T>에 정의된 제약과 맞지 않게 돼요.
  • member this.Summember inline this.Sum으로 바꿔도 형식 제약이 일치하지 않는다는 오류가 나요.

이런 상황에서 필요한 것은 "공중에 떠 있는" 정적 메서드, 즉 형식을 확장하는 것처럼 보여줄 수 있는 정적 메서드예요. 바로 여기서 확장 메서드가 필요해지는 거예요.

확장 메서드(Extension methods)

마지막으로, 확장 메서드("C# 스타일 확장 멤버"라고도 해요)는 F#에서 클래스의 정적 멤버 메서드로 선언할 수 있어요.

확장 메서드는 형식 변수에 제약을 거는 제네릭 형식을 확장하고 싶을 때 유용해요. 예를 들어 볼게요.

namespace Extensions

open System.Collections.Generic
open System.Runtime.CompilerServices

[<Extension>]
type IEnumerableExtensions =
    [<Extension>]
    static member inline Sum(xs: IEnumerable<'T>) = Seq.sum xs

Extensions가 열려 있거나 범위 안에 있으면, 이 코드를 쓸 때 SumIEnumerable에 정의된 것처럼 보여요.

이 확장을 VB.NET 코드에서도 쓸 수 있게 하려면, 어셈블리 수준의 ExtensionAttribute가 하나 더 필요해요.

module AssemblyInfo
open System.Runtime.CompilerServices
[<assembly:Extension>]
do ()

그 밖의 참고 사항(Other remarks)

형식 확장에는 다음과 같은 특징도 있어요.

  • 접근할 수 있는 형식이면 어떤 형식이든 확장할 수 있어요.
  • 내장·선택 형식 확장은 메서드뿐 아니라 어떤 멤버 형식이든 정의할 수 있어요. 예를 들어 확장 속성(extension property)도 가능하죠.
  • 구문self-identifier 토큰은 일반 멤버에서처럼, 호출되는 형식의 인스턴스를 나타내요.
  • 확장 멤버는 정적 멤버나 인스턴스 멤버가 될 수 있어요.
  • 형식 확장의 형식 변수는 선언된 형식의 제약과 일치해야 해요.

형식 확장에는 다음과 같은 제약도 있어요.

  • 형식 확장은 가상(virtual) 또는 추상(abstract) 메서드를 지원하지 않아요.
  • 형식 확장은 확장(override) 메서드를 augmentation으로 지원하지 않아요.
  • 형식 확장은 정적으로 확인되는 형식 매개변수(Statically Resolved Type Parameters)를 지원하지 않아요.
  • 선택 형식 확장은 생성자(constructor)를 augmentation으로 지원하지 않아요.
  • 형식 약어(type abbreviations)에는 형식 확장을 정의할 수 없어요.
  • byref<'T>에는 형식 확장이 유효하지 않아요(선언 자체는 가능하지만요).
  • 특성(attribute)에는 형식 확장이 유효하지 않아요(역시 선언은 가능하지만요).
  • 같은 이름의 다른 메서드를 오버로드하는 확장을 정의할 수는 있지만, 호출이 모호할 때 F# 컴파일러는 확장이 아닌 메서드를 우선해요.

마지막으로, 한 형식에 내장 형식 확장이 여러 개 있으면 모든 멤버 이름이 고유해야 해요. 선택 형식 확장에서는 같은 형식을 확장하는 서로 다른 확장의 멤버가 같은 이름을 가질 수 있어요. 모호성(ambiguity) 오류는 클라이언트 코드가 같은 멤버 이름을 정의하는 서로 다른 두 범위를 동시에 열 때만 발생해요.

더 알아보기