타입 유추(Type Inference)
타입 유추(Type Inference)
F# 컴파일러가 값·변수·매개변수·반환값의 타입을 어떻게 알아내는지 설명드릴게요. 명시적인 타입 표기를 거의 쓰지 않아도 되는 그 편리함의 원리를 쉽게 풀어 보겠습니다.
본문
타입 유추란 (Type Inference in General)
타입 유추의 핵심 아이디어는 간단해요. 컴파일러가 타입을 확실하게 알아내지 못하는 상황이 아니라면, F# 구성 요소의 타입을 굳이 지정하지 않아도 된다는 뜻입니다. 명시적인 타입 정보를 생략한다고 해서 F#이 동적 타입 언어(dynamically typed language)라는 뜻도, 값이 약한 타입(weakly typed)이라는 뜻도 아니에요.
F#은 정적 타입 언어라서 컴파일 과정에서 각 구성 요소의 정확한 타입을 컴파일러가 알아냅니다. 그런데 만약 각 구성 요소의 타입을 알아낼 정보가 부족하다면, 보통 코드 어딘가에 명시적인 타입 주석(type annotation)을 추가해서 필요한 타입 정보를 더 채워 넣어야 합니다.
매개변수와 반환 타입의 유추 (Inference of Parameter and Return Types)
매개변수 목록에서 각 매개변수의 타입을 반드시 지정할 필요는 없어요. 그래도 F#은 정적 타입 언어이기 때문에 컴파일 시점에 모든 값과 식은 반드시 정해진 타입을 가집니다. 내가 명시적으로 지정하지 않은 타입은 컴파일러가 문맥을 보고 유추합니다. 타입을 달리 지정하지 않으면 그 타입은 일반화된(generic) 타입으로 유추됩니다. 만약 어떤 값을 일관성 없이 사용해서, 그 값의 모든 사용처를 만족시키는 단일 타입이 존재하지 않는다면, 컴파일러는 오류를 알려줍니다.
함수의 반환 타입은 함수 안의 마지막 식의 타입으로 결정됩니다.
예를 들어 아래 코드에서는 리터럴 100이 int 타입이므로, 매개변수 a, b와 반환 타입 모두 int로 유추됩니다.
let f a b = a + b + 100
리터럴을 바꾸면 타입 유추 결과도 달라져요. 100 뒤에 접미사 u를 붙여 uint32로 만들면, a, b, 반환값의 타입이 전부 uint32로 유추됩니다.
또한 특정 타입에서만 동작하는 함수나 메서드처럼 타입에 제약을 주는 구성 요소를 사용하는 방법으로도 타입 유추에 영향을 줄 수 있어요.
이런 제약이 충돌하면 오류가 발생합니다. 아래 예시처럼 함수·메서드 매개변수나 식 안의 변수에 명시적인 타입 주석을 적용해서 타입을 직접 지정할 수도 있어요.
// Type annotations on a parameter.
let addu1 (x : uint32) y =
x + y
// Type annotations on an expression.
let addu2 x y =
(x : uint32) + y
또한 모든 매개변수 뒤에 타입 주석을 붙여 함수의 반환 타입을 명시적으로 지정할 수도 있습니다.
let addu1 x y : uint32 =
x + y
매개변수에 타입 주석이 유용한 대표적인 경우는, 매개변수가 객체 타입이고 그 멤버를 사용하고 싶을 때입니다.
let replace(str: string) =
str.Replace("A", "a")
자동 일반화 (Automatic Generalization)
함수 코드가 매개변수의 타입에 의존하지 않는다면, 컴파일러는 그 매개변수를 일반화된(generic) 타입으로 간주합니다. 이것을 **자동 일반화(automatic generalization)**라고 해요. 코드의 복잡도를 높이지 않으면서 제네릭 코드를 작성할 수 있게 해 주는 강력한 기능입니다.
예를 들어 아래 함수는 어떤 타입의 두 매개변수를 묶어 튜플로 만들어 줍니다.
let makeTuple a b = (a, b)
이 함수의 타입은 다음과 같이 유추됩니다.
'a -> 'b -> 'a * 'b
추가 정보 (Additional Information)
타입 유추에 대해 더 자세한 내용은 F# 언어 명세(F# Language Specification)에 설명되어 있습니다.