2018년 5월 8일 화요일

Range, 범위에 대한 이야기 | Swift

Swift 의 Range 기능은 1.0 이라는 초창기 버전 부터 존재했으며 Swift 버전이 올라감에 따라 여러 가지 기능이 붙거나 세분화 되었습니다. 그런데 제 블로그에서는 별도로 정리를 한 글이 없어서 이 범위(Range)에 대한 것을 정리해 봅니다.

참고로 이 글은 Swift 4.0 을 기준으로 쓰여 졌습니다.

Closed Range

ClosedRange 라는 타입은 가장 기본적인 범위 타입으로 시작 값과 끝 값이 명확한 범위를 나타내는 타입입니다. 보통은 이 타입 이름을 그대로 쓰지는 않고 아래처럼 범위 오퍼레이터를 이용해서 값을 생성합니다.
1...10     // 1 부터 10 까지
1..<10     // 1 부터 9 까지
1...10 은 1 이상 10 이하의 범위라는 의미이고, 1..<10 은 1이상 10 미만 범위를 의미합니다.

위의 표현식은 실제로는 ClosedRange 라는 타입으로 생성됩니다.

ClosedRange 타입은 범위 표현 이라는 목적에 맞게 여러 기능이 제공됩니다. 아래는 그 예시입니다.
(1...10).lowerBound // 1
(1...10).upperBound // 10
(1...10).count      // 10
(1...10).isEmpty    // false
굳이 설명이 필요할까 싶을 정도로 이름만 봐도 기능을 알 수 있는 프로퍼티 들입니다. 그런데 솔직히 isEmpty 는 쓸 일이 있을지는 좀 의문이긴 하네요.

count 라는 이름 때문에 배열(Array)와 비슷한 것으로 생각 할 수도 있는데, Range 는 말 그대로 범위를 다루지 해당 범위 내의 여러 아이템의 실체는 존재하지 않습니다. 그저 최소 최대 값 만이 존재하는 타입이지요.

그런데 배열과 비슷하게 범위에도 contains() 메소드가 존재합니다. 동작은 조금 다르지만 의미는 비슷하지요. 예를 들어 특정 값(value)이 있다고 칩시다.
let value = 5
이 값이 1 이상 10 미만인지 판단하려면
1 <= value <= 10
이런 식으로 코드를 쓰고 싶겠지만 이런 식의 코드는 에러가 나니 [...] 보통은 아래 처럼 쓸 것입니다.
1 <= value && value <= 10
솔직히 보기에 좀 안좋은 것 같은데, 이를 아래처럼 범위 표현식을 이용해 좀 다르게 표현 할 수도 있습니다.
(1...10).contains(value)  // true
사람에 따라 다를 수도 있겠지만, 원래 목적에 의한다면 전 이 방식이 더 읽기가 편하다고 느낍니다. 왜냐하면 논리비교식은 꺽쇠의 방향을 얼핏 반대로 오해할 수도 있거든요. 하지만 범위표현식에서는 이런 착각이 발생할 가능성은 훨신 적다고 보기 때문입니다.

참고로 contains() 메소드 대신 ~= 오퍼레이터를 이용 할 수도 있습니다.
1...10 ~= 5 // true
위의 contains() 를 쓰는 것과 동일한 코드입니다. 결과적으로 조금 더 표현이 단순해 지는데, 개인적으로는 생판 모르는 상황에선 오해할 수도 있는 코드 일 수도 있기에 추천하지는 않습니다. 물론 알고 있다면 편하게 쓸 수 있긴 하겠지만요.

앞서 count 프로퍼티에 대한 이야기를 하면서 배열 같다는 이야기를 했는데, 배열 처럼 이터레이션을 하는 특수한(?) 방법을 제공합니다.
(1...10).forEach { value in
    print(value)
}
forEach 라는 메소드를 통해 클로저로 이터레이션을 구현한 예제입니다. 위 코드는 아래와 동일한 역활을 한다고 볼 수 있습니다.
for value in 1...10 {
    print(value)
}
위 예제 중 위의 것은 함수형 프로그래밍의 모습에 가까워 보이고 아래쪽은 전통적인 for 이터레이션이라는 차이가 있겠지요.

위의 인터레이션이 가능한 것은 이 범위 타입이 갯수를 샐 수 있는 타입이라 가능합니다. 이 표현을 영어로 Countable Closed Range 라고 쓸 수 있을 것 같은데 실제로 이 타입이 존재하는 것은 아닙니다. 대신 ClosedRange<Int> 타입의 경우 기본적으로 countable 즉 갯수를 샐 수 있는 것으로 자동으로 판단하는 것 같습니다.

왜 countable 이야기를 하냐 하면 아래와 같은 실수 범위도 생각 해 볼 수 있기 때문입니다.
1.0...10.0
위 범위는 ClosedRange<Double> 이라는 타입으로 생성됩니다. 그런데 이 값은 .count 프로퍼티나 .forEach 메소드를 이용 할 수가 없습니다. 아예 해당 멤버가 없다는 컴파일 오류가 발생합니다.

생각해 보면 당연합니다. 정수형의 경우 최소단위가 1 이기 때문에 A라는 숫자의 다음 숫자는 A + 1 이라는 식으로 정의가 가능합니다. 하지만 Double 같은 실수 타입에는 최소단위라는 개념이 없습니다. 따라서 범위 내부에 어떠한 값들이 얼마나 있는지 정의 한다는 건 불가능한 이야기지요.

범위 타입은 앞서 본 것 처럼 제너릭(Generics)이기 때문에 여러 가지 타입으로 정의가 가능합니다. 아래 처럼 Date 타입으로 범위를 만들 수도 있습니다.
let now = Date()
let future = now.addingTimeInterval(3600)
let period = now...future
이렇게 만들어진 기간 범위를 이용해 날짜가 해당 범위에 포함되는지 간단하게 체크 할 수 있습니다.
period.contains(Date().addingTimeInterval(10))
개인적으로 날짜 비교에서 영어 표현에 익숙하지 않아서 곤욕스러울 때가 있는데 범위를 이용하는 방식이 훨신 이해가 잘 되는 것 같습니다.

범위에 쓸 수 있는 타입은 사실 Comparable 을 구현한 타입이면 전부 쓸 수 있습니다. 예를 들어 문자열로 범위를 만들수도 있고, 커스텀 클래스나 구조체가 Comparable 을 따르기만 하면 그것으로도 범위를 만들 수 있습니다.

Partial Range

Partial Range 는 특수한 범위 타입으로 시작이나 끝이 생략된 형태의 범위를 의미합니다. 닫힌 범위(Closed Range) 가 있었으니 이에 대치되는 열린 범위(Opened Range) 라는 이름일거라 생각했다면 실패했네요. :-P

하여간 이 시작값이나 끝 값이 생략된 범위(Partial Range)는 아래와 같은 식으로 표현합니다.
...10     // PartialRangeThrough<Int>
10...     // CountablePartialRangeFrom<Int>
..<10     // PartialRangeUpTo<Int>
세 가지를 나열했는데, Partial Range 타입도 여러 종류가 있고 여기서는 이 중 세 가지를 꼽아 본 것입니다. 보시다시피 앞의 Closed Range 의 한 쪽이 사라진 형태입니다.

각각의 의미는 코드만 봐도 대체로 이해 가능하실 겁니다. ...10 은 10 이하 라는 범위이고, 10... 은 10 이상 이라는 범위, 그리고 마지막은 10 미만 이라는 범위입니다.

이 타입도 contains() 같은 메소드가 제공됩니다.
(10...).contains(15)  // true
(10...).contains(5)   // false
그런데 이 경우라면 굳이 범위식 보다는 그냥 단순 논리식이 편한 것 같습니다.
15 >= 10   // true
5 >= 10    // false
꺽쇠 방향을 착각하면 오해가 생기겠지만, 이 Partial Range 자체도 어느 방향에 값이 있냐에 따라 착각이 생길 수 있어서 뭐가 더 낫냐 라는 판단은 보류 하겠습니다. -_-;

시작값(lower bound)이나 끝값(upper bound)이 없으니 이터레이션(forEach)이나 갯수(count) 같은건 못 쓰겠네 라고 생각 할 수 있는데, 여기서 10... 은 Countable 이름이 붙어 있다는 점에 주의해서 봅시다. 이 녀석만은 count 나 forEach 를 왠지 사용 할 수 있을 것 같습니다. 실제로도 underestimatedCount 라는 count 라는 비슷한(?) 프로퍼티가 제공되고, forEach 도 예외처리가 필요한 것을 제외하곤 역시 사용이 가능합니다.

왜 굳이 10... 만 countable 로 표기되는지는 명확하지 않습니다. 기본적인 컴파일러 동작으로 정수형이면서 최소값이 정해져 있으면 제한적으로 동작이 가능하다는 식으로 정의가 되어 있는 것 같습니다.

마무리

개인적으로 범위 표현을 좀 좋아하지 않았었습니다. 왜냐하면 모호한 표기 때문에 어쩔 수 없이 괄호를 써야 하는 점이 싫었기 때문이지요. 예를 들어 앞서 예제로 썼던 코드를 봅시다.
(1...10).forEach { ... }
이 코드에서는 괄호를 안쓰면 컴파일 에러가 닙니다. 괄호를 써야 하는 것이 현실이라는 말이지요.

물론 범위 자체를 다른 변수에 넣어서 쓰면 되기 하지만 일시적으로 한번만 쓰는 경우가 많아서 낭비하는 기분이 들기도 했구요.

그런데 이 글을 쓰게 되면서, 특히 Date 타입을 범위에 쓰는 예제를 알게 된 이후로 범위에 대한 선호가 약간은 달라질 것 같습니다.

글을 쓸다는 것은 자기 자신에게 공부도 되기에 참 좋은 학습 방법 같습니다. :-)

[관련글] 스위프트(Swift) 가이드

댓글 없음 :