타입
타입은 값이 어떤 종류인지를 나타냅니다. 이 값이 문자열일까요, 정수일까요, 아니면 어떤 복잡한 구조체일까요? 값의 타입이 바로 그 답을 알려 줍니다. 엄격한 타입 시스템은 컴파일 도중에 큰 부류의 버그를 없애고, 다른 많은 버그를 잡아 주는 강력한 도구입니다.
Reason에선 거의 대부분의 타입이 추론될 수 있습니다. 컴파일러는 프로그램 속 모든 것의 타입을 추론하고, 추론한 타입이 말이 되도록 보장할 것입니다. 따라서 우리는 모든 값의 타입을 쓰지 않고도 엄격한 타입 시스템의 이점을 누릴 수 있습니다.
기초
여기 [let binding](let binding.md)이 하나 있습니다. 이름은 count
이고, 타입은 int
, 값은 42
입니다. 우리는 명시적으로 그게 int
라고 쓰지 않았지만, 타입은 추론됩니다.
여러분이 타입을 쓰지 않아도 Reason의 모든 것에는 타입이 있습니다.
let count = 42;
타입은 주석을 통해 명시적으로 추가될 수 있습니다.
let count: int = 42;
count
가 타입을 가지기 때문에 컴파일러는 우리가 이 값을 가지고 무엇을 할 수 있고 없는지를 알고 있습니다.
/* 덧셈은 허용됨 */
let nextCount = count + 1;
/* 오류: count는 list가 아님 */
let x = List.map(fn, count);
주석 (Annotation)
타입 주석은 거의 모든 곳에 사용할 수 있습니다. Reason의 타입 추론 때문에 자주 필요하지는 않지만, 여러분의 프로그램의 타입에 대한 이해를 더 확실히 갖게 하는 데에 도움이 될 수 있습니다.
아래 예시 곳곳에 int
와 string
이 사용되었습니다.
let five: int = 5;
let nine = (five: int) + (4: int);
let add = (x: int, y: int): int => x + y;
let drawCircle = (~radius: int): string => "hi";
별칭 (Alias)
타입에 대해 별칭을 정의할 수 있습니다. 이는 간단한 타입에 의미를 붙일 때와 쓰기엔 너무 길어지는 복잡한 타입을 다룰 때 도움이 됩니다.
type seconds = int;
type timeInterval = (seconds, seconds);
별칭 seconds
를 사용하여 sleep
함수가 어떻게 동작하는지가 분명해졌습니다. int
를 사용했다면 그렇지 않았을 것입니다.
let sleep = (time: seconds) => { ... }
타입 별칭은 variants나 records를 다루는 등 몇몇 경우에 필요합니다.
타입 파라미터
타입은 다른 언어의 제네릭과 비슷하게, 타입 파라미터를 받을 수 있습니다. 파라미터화된 타입은 여러 타입의 값을 다루는 구조를 정의할 때 유용합니다. 인자로 int
, float
, string
을 받는 list
타입 하나를 쓰는 게 세 타입 intList
, floatList
, stringList
을 쓰는 것보다 낫습니다.
타입을 정의할 때 파라미터 앞에는 작은따옴표('
) 하나를 붙입니다.
type list('item) = ...
이 타입을 주석으로써 사용할 때 파라미터 자리에는 구체적인 타입이 채워집니다.
let x: list(int) = [1, 2, 3];
let y: list(string) = ["one", "two", "three"];
타입은 파라미터 여러 개를 가질 수 있고, 중첩될 수도 있습니다.
type pair('a, 'b) = ('a, 'b);
let x: pair(int, string) = (1, "one");
let y: pair(string, list(int)) = ("123", [1, 2, 3]);
- 타입 파라미터 이름을
'a
,'b
,'c
, ... 로 짓는 것이 일반적인 컨벤션입니다. - 타입 파라미터도 추론될 수 있습니다!
불투명 타입
불투명 타입은 유저들에게 노출되는 구현 세부 사항을 제한하는 강력한 도구입니다. 불투명 타입을 사용하면 변경 요구 사항을 맞추도록 더 쉽고 안전하게 구현을 수정할 수 있습니다.
알아두기: 이 예시들에서는 간단함을 위해 모듈 타입을 사용했습니다. 하지만 보통 불투명 타입은 인터페이스 파일을 써서 만듭니다.
준비
Duration.t
는 불투명 타입입니다. 구체적 타입은 숨겨져 있고, 제한된 종류의 함수만을 사용해서 상호작용할 수 있습니다.
(type t;
라고 쓰는 것은 t
를 불투명하게 만듭니다. type t = int;
라고 쓰면 t
는 불투명하지 않고 구체적인 타입을 가질 것입니다.)
module type Duration = {
/* 불투명 타입 */
type t;
let fromSeconds: int => t;
let add: (t, t) => t;
};
module Duration: Duration = {
/* Duration이 몇 초인지 */
type t = int;
let fromSeconds = value => value;
let add = (x, y) => x + y;
};
Duration
모듈을 써서 불투명 타입을 만들고 사용합니다.
let oneMinute = Duration.fromSeconds(60);
let twoMinutes = Duration.add(oneMinute, oneMinute);
보통의 정수 함수는 Duration.t
타입 값을 받으면 일부러 오류를 냅니다.
/* 오류: int를 예상했지만 Duration.t를 받음 */
let twoMinutes = oneMinute + oneMinute;
구현 바꾸기
만약 우리가 duration의 정밀도를 높여서 밀리초 단위의 정밀도를 허용하고 싶다면 자신있게 구현을 바꿔도 아무것도 망가뜨리지 않는다고 확신할 수 있습니다.
module type Duration = {
type t;
let fromSeconds: int => t;
let fromMS: int => t;
let add: (t, t) => t;
};
module Duration: Duration = {
/* Duration in milliseconds */
type t = int;
let fromSeconds = value => value * 1000;
let fromMS = value => value;
let add = (x, y) => x + y;
};
아래 코드는 위에서 했던 것과 완전히 똑같이 동작합니다.
let oneMinute = Duration.fromSeconds(60);
let twoMinutes = Duration.add(oneMinute, oneMinute);
하지만 이제는 1초 미만의 duration도 지원할 수 있습니다.
let halfSecond = Duration.fromMS(500);
let longerThanOneMinute = Duration.add(oneMinute, halfSecond);
이는 코드를 더 쉽게 코드를 관리하고, 더 안전하게 변경하는 데 도움이 되는 기법입니다.