F# 제약 조건(Constraints)

F# 제약 조건(Constraints)

제네릭 타입이나 제네릭 함수를 쓸 때, 타입 인자에 어떤 요구사항을 걸고 싶을 때가 있어요. 예를 들어 "어떤 타입이든 받되, 적어도 비교할 수 있는 타입이어야 한다" 같은 조건 말이죠. 이때 사용하는 게 바로 제약 조건(constraint)이에요. 이 문서에서는 F# 제네릭 타입 매개변수에 적용할 수 있는 제약 조건을 하나씩 살펴볼게요.

출처: F# 문서 - Constraints

본문

구문

type-parameter-list when constraint1 [ and constraint2]

설명

제네릭 타입에 사용할 수 있는 타입을 제한하는 제약 조건은 종류가 여럿 있어요. 아래 표에 그 목록과 설명을 정리했어요.

제약 조건 구문 설명
타입 제약(Type Constraint) type-parameter :> type 제공된 타입이 지정된 타입과 같거나 그 타입에서 파생되어야 해요. 지정된 타입이 인터페이스라면, 제공된 타입이 그 인터페이스를 구현해야 하고요.
Null 제약(Null Constraint) type-parameter : null 제공된 타입이 null 값을 지원해야 해요. 여기에는 모든 .NET 객체 타입이 포함되지만, F#의 list, tuple, function, class, record, union 타입은 빠져요.
Not Null 제약(Not Null Constraint) type-parameter : not null 제공된 타입이 null 값을 지원하면 안 돼요. null로 주석이 달린 타입과, null을 나타내는 값으로 쓰는 타입(option 타입이나 AllowNullLiteral 특성으로 정의된 타입 같은 것)이 모두 제외돼요. 값 타입은 원래 null이 될 수 없으니 이 제약 조건을 통과해요.
명시적 멤버 제약(Explicit Member Constraint) [(]type-parameter [or ... or type-parameter)] : (member-signature) 제공된 타입 인자 중 적어도 하나가 지정된 시그니처를 가진 멤버를 가져야 해요. 일상적으로 쓸 용도는 아니에요. 이 제약의 유효한 대상이 되려면 멤버가 타입에 명시적으로 정의되어 있거나 암시적 타입 확장의 일부여야 해요.
생성자 제약(Constructor Constraint) type-parameter : ( new : unit -> 'a ) 제공된 타입이 매개변수가 없는 생성자를 가져야 해요.
값 타입 제약(Value Type Constraint) type-parameter : struct 제공된 타입이 .NET 값 타입이어야 해요.
참조 타입 제약(Reference Type Constraint) type-parameter : not struct 제공된 타입이 .NET 참조 타입이어야 해요.
열거형 타입 제약(Enumeration Type Constraint) type-parameter : enum<underlying-type> 제공된 타입이 지정된 기본 타입을 가진 열거형이어야 해요. 일상적으로 쓸 용도는 아니에요.
대리자 제약(Delegate Constraint) type-parameter : delegate<tuple-parameter-type, return-type> 제공된 타입이 지정된 인자와 반환 값을 가진 대리자 타입이어야 해요. 일상적으로 쓸 용도는 아니에요.
비교 제약(Comparison Constraint) type-parameter : comparison 제공된 타입이 비교(comparison)를 지원해야 해요.
동등성 제약(Equality Constraint) type-parameter : equality 제공된 타입이 동등성(equality)을 지원해야 해요.
비관리형 제약(Unmanaged Constraint) type-parameter : unmanaged 제공된 타입이 비관리형 타입이어야 해요. 비관리형 타입에는 특정 기본 타입들(sbyte, byte, char, nativeint, unativeint, float32, float, int16, uint16, int32, uint32, int64, uint64, decimal), 열거형, nativeptr<_>, 그리고 필드가 모두 비관리형 타입인 비제네릭 구조체가 포함돼요.

코드가 제약 타입에는 있지만 일반적인 타입에는 없는 기능을 써야 할 때는 제약 조건을 붙여야 해요. 예를 들어 타입 제약 조건으로 클래스 타입을 지정하면, 제네릭 함수나 타입 안에서 그 클래스의 메서드를 아무거나 쓸 수 있어요.

타입 매개변수를 명시적으로 쓸 때 제약 조건이 필요해지는 경우가 있어요. 제약 조건이 없으면 컴파일러가 "런타임에 이 타입 매개변수로 어떤 타입이 들어와도, 네가 쓰는 기능이 그 타입에 있다는 걸" 검증할 방법이 없거든요.

F# 코드에서 가장 흔히 쓰는 건 기본 클래스나 인터페이스를 지정하는 타입 제약이에요. 나머지 제약들은 F# 라이브러리가 특정 기능을 구현할 때 쓰거나(예: 산술 연산자의 연산자 오버로딩을 구현할 때 쓰는 명시적 멤버 제약), 공용 언어 런타임(CLR)이 지원하는 제약 조건의 전체 집합을 F#도 지원하기 때문에 제공되는 경우가 많아요.

타입 유추 과정에서 일부 제약 조건은 컴파일러가 자동으로 유추하기도 해요. 예를 들어 함수에서 + 연산자를 쓰면, 컴파일러가 식에 사용된 변수 타입에 명시적 멤버 제약을 자동으로 추론해요.

다음 코드는 제약 조건 선언의 여러 예시를 보여줘요.

// Base Type Constraint
type Class1<'T when 'T :> System.Exception> =
    class end

// Interface Type Constraint
type Class2<'T when 'T :> System.IComparable> =
    class end

// Null constraint
type Class3<'T when 'T : null> =
    class end

// Not Null constraint
type Class4<'T when 'T : not null> =
    class end

// Member constraint with instance member
type Class5<'T when 'T : (member Method1 : 'T -> int)> =
    class end

// Member constraint with property
type Class6<'T when 'T : (member Property1 : int)> =
    class end

// Constructor constraint
type Class7<'T when 'T : (new : unit -> 'T)>() =
    member val Field = new 'T()

// Reference type constraint
type Class8<'T when 'T : not struct> =
    class end

// Enumeration constraint with underlying value specified
type Class9<'T when 'T : enum<uint32>> =
    class end

// 'T must implement IComparable, or be an array type with comparable
// elements, or be System.IntPtr or System.UIntPtr. Also, 'T must not have
// the NoComparison attribute.
type Class10<'T when 'T : comparison> =
    class end

// 'T must support equality. This is true for any type that does not
// have the NoEquality attribute.
type Class11<'T when 'T : equality> =
    class end

type Class12<'T when 'T : delegate<obj * System.EventArgs, unit>> =
    class end

type Class13<'T when 'T : unmanaged> =
    class end

// Member constraints with two type parameters
// Most often used with static type parameters in inline functions
let inline add(value1 : ^T when ^T : (static member (+) : ^T * ^T -> ^T), value2: ^T) =
    value1 + value2

// ^T and ^U must support operator +
let inline heterogenousAdd(value1 : ^T when (^T or ^U) : (static member (+) : ^T * ^U -> ^T), value2 : ^U) =
    value1 + value2

// If there are multiple constraints, use the and keyword to separate them.
type Class14<'T,'U when 'T : equality and 'U : equality> =
    class end

더 알아보기