자동 일반화
자동 일반화 (Automatic Generalization)
F#은 타입 추론(type inference)을 이용해 함수와 식의 타입을 평가해요. 이 문서에서는 F#이 함수의 인자와 타입을 어떻게 자동으로 일반화해서, 가능할 때 여러 타입에서 동작할 수 있게 만드는지 설명할게요.
출처: https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/generics/automatic-generalization
본문
자동 일반화 (Automatic Generalization)
F# 컴파일러는 함수에 대해 타입 추론을 수행할 때, 주어진 매개변수가 제네릭(일반화)이 될 수 있는지 판단해요. 컴파일러는 각 매개변수를 살펴보고 함수가 그 매개변수의 특정 타입에 의존하는지 결정해요. 의존하지 않는다면 그 타입은 제네릭으로 추론돼요.
다음 코드 예시는 컴파일러가 제네릭으로 추론하는 함수를 보여줘요.
let max a b = if a > b then a else b
이 함수의 타입은 'a -> 'a -> 'a 로 추론돼요.
이 타입은 "같은 미지의 타입 인자 두 개를 받아 그와 같은 타입의 값을 반환하는 함수"임을 뜻해요. 위 함수가 제네릭이 될 수 있는 이유 중 하나는 크다 연산자(>) 자체가 제네릭이기 때문이에요. 크다 연산자의 시그니처는 'a -> 'a -> bool 이에요. 모든 연산자가 제네릭인 건 아니에요. 함수의 코드에서 매개변수 타입을 비-제네릭 함수나 연산자와 함께 사용한다면, 그 매개변수 타입은 일반화될 수 없어요.
max가 제네릭이기 때문에 아래 예시처럼 int, float 등 다양한 타입과 함께 사용할 수 있어요.
let biggestFloat = max 2.0 3.0
let biggestInt = max 2 3
다만 두 인자는 같은 타입이어야 해요. 시그니처가 'a -> 'b -> 'a가 아니라 'a -> 'a -> 'a이기 때문이에요. 따라서 다음 코드는 타입이 일치하지 않아 오류를 발생시켜요.
// Error: type mismatch.
let biggestIntFloat = max 2.0 3
max 함수는 크다 연산자를 지원하는 어떤 타입에서도 동작해요. 그래서 아래 코드처럼 문자열에도 사용할 수 있어요.
let testString = max "cab" "cat"
값 제한 (Value Restriction)
컴파일러는 명시적 인자를 가진 완전한 함수 정의와, 단순한 불변(immutable) 값에 대해서만 자동 일반화를 수행해요.
즉, 특정 타입으로 충분히 제한되지 않았으면서도 일반화할 수도 없는 코드를 컴파일하려 하면 컴파일러가 오류를 내요. 이 문제에 대한 오류 메시지는 값에 대한 자동 일반화의 이런 제한을 **값 제한(value restriction)**이라고 불러요.
일반적으로 값 제한 오류는 두 가지 경우에 발생해요. 어떤 구문(構文)이 제네릭이 되길 원하지만 컴파일러가 일반화할 충분한 정보를 갖지 못할 때, 또는 비-제네릭 구문에서 실수로 충분한 타입 정보를 생략했을 때요. 값 제한 오류를 해결하는 방법은 타입 추론 문제를 더 충분히 제한하도록 더 명시적인 정보를 제공하는 거예요. 다음과 같은 방법이 있어요.
- 값이나 매개변수에 명시적 타입 주석(annotation)을 추가해 타입을 비-제네릭으로 제한한다.
- 문제가 함수 합성이나 부분 적용된 커리드(curried) 함수 인자 같은 일반화할 수 없는 구문으로 제네릭 함수를 정의한 것이라면, 함수를 보통의 함수 정의로 다시 작성해 본다.
- 문제가 일반화하기엔 너무 복잡한 식이라면, 사용하지 않는 인자를 하나 추가해 그 식을 함수로 만든다.
- 명시적 제네릭 타입 매개변수를 추가한다. (이 방법은 거의 사용되지 않아요.)
다음 코드 예시들이 각각의 경우를 보여줘요.
사례 1: 너무 복잡한 식. 이 예시에서 counter 목록은 int option ref 가 되도록 의도했지만, 단순한 불변 값으로 정의되지는 않았어요.
let counter = ref None
// 타입 주석을 추가하면 문제가 해결돼요:
let counter : int option ref = ref None
사례 2: 일반화할 수 없는 구문으로 제네릭 함수 정의하기. 이 예시에서 구문은 함수 인자의 부분 적용을 포함하기 때문에 일반화할 수 없어요.
let maxhash = max << hash
// 아래는 maxhash의 인자가 명시적이므로 허용돼요:
let maxhash obj = (max << hash) obj
사례 3: 사용하지 않는 인자를 하나 추가하기. 이 식은 일반화하기에 충분히 단순하지 않기 때문에 컴파일러가 값 제한 오류를 발생시켜요.
let emptyList10 = Array.create 10 []
// (사용하지 않는) 인자를 하나 추가하면 함수가 되어 일반화할 수 있어요.
let emptyList10 () = Array.create 10 []
사례 4: 타입 매개변수 추가하기.
let arrayOf10Lists = Array.create 10 []
// 타입 매개변수와 타입 주석을 추가하면 제네릭 값을 작성할 수 있어요.
let arrayOf10Lists<'T> = Array.create 10 ([]:'T list)
마지막 사례에서 그 값은 타입 함수가 되어, 예를 들어 다음과 같이 다양한 타입의 값을 만드는 데 사용할 수 있어요.
let intLists = arrayOf10Lists<int>
let floatLists = arrayOf10Lists<float>