Dart 널 가능성 문법 결정: a?[b]일까 a?.[b]일까
Dart 널 가능성 문법 결정: a?[b]일까 a?.[b]일까
Dart가 널 가능성(nullability) 타입 시스템을 설계하면서 내린 문법 선택을 뒷이야기와 함께 풀어낸 글입니다. 널 안전성(NNBD) 도입 과정에서 서브스크립트([]) 연산자의 널 인식 문법을 어떻게 정했는지, 그리고 왜 그렇게 정했는지 설명해요. 참고로 2021년 3월 업데이트 덕분에, 최종 결정은 a?[b] 문법을 쓰는 쪽으로 났어요.
본문
업데이트(2021/3): 여러분의 피드백 덕분에, Dart 팀은
a?[b]문법을 사용하기로 결정했어요.
Dart는 타입 시스템을 재설계하는 중이에요. 그래서 개별 타입이 널 가능(nullable, 그 타입의 표현식이 값으로 null을 가질 수 있음)이거나 널 불가능(non-nullable)하게 될 거예요. 올해 연말쯤 일정과 롤아웃 과정을 더 자세히 알려드릴게요. 어쨌든 결국 Dart 코드는 기본적으로 널 불가능(NNBD)이 되고, 타입이 널 값을 허용한다는 걸 표현하려면 특별한 문법을 써야 해요.
예를 들어 정수가 널일 수 있다고 선언하려면, 타입 뒤에 ?를 붙여야 해요:
int? someInt; // someInt can be null.
이 물음표 문법은 Kotlin, Swift, C# 코드를 본 적 있다면 익숙할 거예요. 그런데 몇 가지는 다르죠. 특히 서브스크립트([]) 연산자가 그래요. 이건 리스트나 배열 접근에 가장 흔히 쓰이는 연산자예요. C#과 Swift는 ?[]를 써요. Dart(그리고 참고로 ECMAScript도)의 현재 계획은 ?.[]를 쓰는 거예요:
e1?.[e2] // null if e1 is null; otherwise it's e1[e2]
이 글은 이 결정 뒤에 숨은 이유를 무대 뒤에서 설명하고, 여러분의 생각과 제안을 나눠달라고 권하기 위한 거예요. 이 내용의 대부분은 Bob Nystrom(munificent)이 언어 이슈 #376에 남긴 코멘트에 기반해요. 그 코멘트는 Bob과 NNBD 스펙의 주인인 Leaf Petersen(leafpetersen) 사이의 논의를 요약한 것이죠.
왜 점(dot)을 쓸까
[]에 대한 두 선택지 모두 장점이 있어요.
e1?[e2]:
- C#과 Swift를 따른다
- 간결하다
!연산자 문법과 비슷하다 (식 뒤에 붙여서, 널 가능 타입이어도 그 값이 널이 아니라고 말해주는 연산자예요):e1![e2]
e1?.[e2]:
- 캐스케이드 문법과 비슷하다:
e1..[e2] // cascade syntax e1?..[e2] // null-aware cascade syntax - 다른 널 인식 메서드 문법과 비슷하다:
e1?.e2() - 다른 연산자로 자연스럽게 확장된다
- 다음 코드의 모호성을 피한다:
{ e1 ? [e2] : e3 }
?[의 모호성을 피할 방법을 찾는 데 한동안 시간을 썼어요. 문제는 { e1 ? [e2] : e3 } 같은 코드가 조건식의 결과를 담은 집합(set) 리터럴인지, 널 인식 서브스크립트의 결과를 담은 맵(map) 리터럴인지 알 수 없다는 거예요.
그 코드를 명확하게 만들기 위해 괄호를 추가한다면, 전체 표현식 주위에 괄호를 붙여 { (e1 ? [e2] : e3) }로 만들어 명확히 집합 리터럴로 하거나, 앞부분 주위에 괄호를 붙여 { (e1 ? [e2]) : e3 }로 만들어 명확히 맵 리터럴로 할 수 있어요. 하지만 괄호가 없으면 파서는 무엇을 해야 할지 알 수 없어요.
이 모호성에는 여러 해결책이 있지만, 만족스러운 건 없어 보여요. 한 가지 접근은 공백에 의존해 선택지를 구분하는 거예요. e1 ? [e2]는 ?와 [ 사이에 공백이 있으니 항상 조건식의 시작으로 취급하고, e1?[e2]는 두 토큰 사이에 공백이 없으니 항상 널 인식 서브스크립트로 취급하는 방식이죠. 하지만 공백에 의존하는 건 사용자 경험에 정말 해로울 수 있어요.
이론상으로는 포맷된 코드에서는 공백에 의존하는 게 문제가 아니에요. 하지만 많은 사용자가 포맷되지 않은 Dart 코드를 포매터의 입력으로 넣어요. 그러면 그 입력 형식이 언어의 이 모서리에서 더 공백에 민감하고 깨지기 쉬워지겠죠. 지금까지 Dart는 그런 모서리가 아주 드물어서, 이건 좋은 특징이에요. (그런 모서리 하나가 — — a와 --a가 둘 다 유효하지만 뜻이 다른 경우예요.)
모호성을 무시하더라도, 점을 쓸 또 다른 이유가 있어요. 다른 연산자에도 널 인식 형태를 추가한다면(e1?.+(e2) 등) 점을 요구하게 될 텐데, 그러면 서브스크립트에도 점을 요구하는 게 그 미래와 일관적이기 때문이에요.
NNBD에 대해 논의한 또 다른 추가는 널 인식 호출 문법이에요. 거기서 점을 요구하지 않으면 정확히 같은 모호성 문제가 생겨요:
var wat = { e1?(e2):e3 }; // Map or set?
?[에 대해 만들어낸 어떤 해결책이든 ?(에도 적용해야 해요.
마지막으로, 서브스크립트를 체이닝하는 이 예시를 생각해볼게요:
someJson?[recipes]?[2]?[ingredients]?[pepper]
우리 눈에는 그게 별로 안 좋아 보여요. 메서드 체인처럼 읽히기보다는 중위 연산자 몇 개가 섞인 것처럼 읽히죠. 조금 ??와도 비슷하고요. 비교를 위해 이 코드를 볼게요:
someJson?.[recipes]?.[2]?.[ingredients]?.[pepper]
이쪽은 더 분명하게 메서드 체인처럼 보여요. 그것을 시각적으로 전달하는 것도 중요해요. 사용자가 표현식의 어느 정도가 널에 의해 단락(short-circuit)될지 빠르게 이해해야 하기 때문이에요.
이 모든 것을 종합하면, 우리는 다음 이유들로 ?.[ 형태를 써야 한다고 보여요:
- 모호성 문제를 피한다. (lexer는 이미
?.을 단일 '널 인식' 토큰으로 취급한다.) - 널 인식 호출로 자연스럽게 확장된다.
- 다른 널 인식 연산자로 확장된다.
- Dart를 포매터에게 더 견고한 입력 언어로 남겨준다.
- 메서드 체인에서 꽤 보기 좋다.
- 메서드 체인의 어느 정도가 단락될지 시각적으로 더 명확하다.
여러분의 생각은 어떤가요
우리는 언제나 피드백과 제안에 관심이 있어요. 이 문법에 피드백을 주는 가장 좋은 방법은 언어 이슈 #376에 코멘트를 남기거나 코멘트에 좋아요를 누르는 거예요. 그 김에 우리가 작업 중인 다른 멋진 언어 기능들도 살펴봐 주세요.
더 많은 정보를 찾을 수 있는 곳이에요:
NNBD:
- 기능 스펙(feature specification)
- 로드맵(roadmap)
- 이슈(issues)
기타 Dart 언어 변경과 기능:
- 언어 변경 상태(status of language changes)
- 언어 진화 과정(language evolution process)
- Dart 언어 스펙(language specification)