여러 프로그래밍 언어가 세미콜론 없이도 문을 구분하기 위해 줄바꿈을 어떻게 해석하는지 비교하고, 그 설계 선택이 갖는 장단점을 정리한다.
Posted on 18 March 2026
나는 Roto라는 스크립팅 언어를 만들고 있다. 이전의 아주 많은 프로그래밍 언어들처럼, 사용하기 쉽고 읽기 쉽다는 목표를 갖고 있다. 많은 언어들이 그 목적을 위해 세미콜론으로 문을 구분하거나 종료하는 것을 선택 사항으로 만든다. 나도 그걸 원한다!
말은 쉬운데, 그걸 어떻게 구현할까? 명시적인 종료 기호 없이 문이 어디서 끝나는지 어떻게 결정할까? 문제를 설명하기 위해, 표현식을 조금 이상하게 포맷해 보자. Rust 예제로 시작해 보자:
fn foo(x: u32) -> u32 {
let y = 2 * x
- 3;
y
}
Rust에서는 완전히 모호하지 않다. 이제 Python에서 같은 일을 해보자:
def foo(x):
y = 2 * x
- 3
return y
"unexpected indent" 오류가 난다! Python은 세미콜론을 요구하지 않기 때문에 혼란스러워한다. 알고 보니 많은 언어들이 이 문제에 대해 서로 다른 해법을 갖고 있다. 예를 들어 Gleam은 이렇게 된다:
fn foo(x) {
let y = 2 * x
- 3
y
}
이건 허용된다! 그리고 echo foo(4)를 하면 Rust처럼 5가 나온다. 그럼 Gleam은 어떻게 해서 표현식이 두 번째 줄에서도 계속된다고 판단할까?
나는 이런 차이가 중요하다고 생각한다. 특히 프로그래밍 언어 설계에 관심이 있다면 더더욱. 언어의 문법은 프로그래머에게 직관적이고 명확해야 해서, 표현식이 어떻게 파싱되는지 자신 있게 설명할 수 있어야 한다.
보통 그런 문법 규칙은 명확하다. 많은 언어에서 함수 인자는 ()로 구분된다는 식이다. 하지만 줄바꿈으로 분리된 문에 대한 규칙은 종종 더 모호하고 언어마다 다르다. 사용자들은 대개 방어적으로 세미콜론을 넣으라고 하거나, 그냥 걱정하지 말라고 듣는다. 내게는 둘 다 (사소하긴 하지만) 언어 설계의 실패처럼 보인다.
그렇다면 Roto에 이런 문제 없이 적용할 접근법은 어떻게 찾을까? 나는 가장 좋은 방법이 11(!)개의 언어가 무엇을 하고 있는지, 그리고 그 접근이 어떻게 비교되는지 살펴보는 것이라고 판단했다. 이 글은 그 탐색이다. 무엇이 최선인지에 대한 확실한 답은 없지만, 그래도 유익한 개요가 되길 바란다.
NOTE: 아래의 모든 언어에 능통한 것은 아니다. 사실 어떤 것들은 거의 써본 적도 없다. 가능하면 출처를 인용하려 했지만, 여전히 몇몇 세부사항을 틀렸을 수도 있다. 실수를 발견하면 알려 달라!
이 글은 길기 때문에, 좋아하는 언어로 바로 넘어가고 싶을 수도 있겠다. 그래서 모든 섹션의 링크를 여기에 모아 둔다:
공백 민감성으로 가장 유명한 언어부터 시작하자([citation needed]). Python은 한 줄이 곧 한 문이라고 가정한다. Python의 grammar에서는 이를 _logical lines_라고 부른다. 이는 에디터에서 보이는 줄인 physical lines 하나 이상으로 구성된다.
physical line을 결합하는 방법은 2가지가 있다:
\ 토큰으로 명시적으로 결합하거나,(), [], {} 또는 삼중 따옴표 같은 구분자 안에 들어 있을 때 암시적으로 결합한다.레퍼런스에는 다음 예시가 있다:
# Explicit joining
if 1900 < year < 2100 and 1 <= month <= 12 \
and 1 <= day <= 31 and 0 <= hour < 24 \
and 0 <= minute < 60 and 0 <= second < 60: # Looks like a valid date
return 1
# Implicit joining
month_names = ['Januari', 'Februari', 'Maart', # These are the
'April', 'Mei', 'Juni', # Dutch names
'Juli', 'Augustus', 'September', # for the months
'Oktober', 'November', 'December'] # of the year
이 규칙들만 있으면 꽤 오류가 나기 쉬울 것이다. 서두의 예시를 다시 생각해 보면 알 수 있다:
y = 2 * x
- 3
첫 줄 끝에 백슬래시를 잊으면, Python은 그걸 그냥 두 개의 문으로 취급해 버린다. 다행히 Python에는 해결책이 있다. 들여쓰기를 엄격하게 강제하는 것이다. - 3이 새 줄에 있으므로, 바로 앞 줄과 같은 들여쓰기를 가져야 한다.
이제 Python 접근의 결과를 생각해 보자. 이 방식은 문 분리에 대해 꽤 원칙적이고 엄격하다. 그리고 매우 모호함이 없다. 프로그래밍할 때 "한 줄, 한 문" 규칙을 머리에 유지하기 쉽고, 두 가지 예외도 꽤 명시적이다.
하지만 들여쓰기 기반 언어치고는 다소 아이러니한 결과가 있는데, Python의 규칙이 커뮤니티로 하여금 명시적 구분자를 더 선호하게 만들었다는 점이다. 예를 들어 널리 쓰이는 포매터 black과 ruff는 둘 다 백슬래시보다 괄호를 선호한다.
# "Unidiomatic"
y = long_function_name(1, x) \
+ long_function_name(2, x) \
+ long_function_name(3, x) \
+ long_function_name(4, x)
# "Idiomatic"
y = (
long_function_name(1, x)
+ long_function_name(2, x)
+ long_function_name(3, x)
+ long_function_name(4, x)
)
나는 Python의 시스템이 꽤 좋다고 생각한다! 단순하고, 명확하고, 들여쓰기 규칙이 실수를 잡아낼 가능성이 크다. Python을 쓰던 시절을 돌아보면, 표현식을 ()로 감싸야 하는 경우를 제외하면 크게 방해받았던 기억이 없다. 이런 동작에 놀란 적도 거의 없었다.
Sources:
Go의 접근은 Python과 매우 다르다. Go의 official book에는 이렇게 적혀 있다:
Like C, Go's formal grammar uses semicolons to terminate statements, but unlike in C, those semicolons do not appear in the source. Instead, the lexer uses a simple rule to insert semicolons automatically as it scans, so the input text is mostly free of them.
여기서 내가 싫어하는 첫 번째 점은, 문이 종료된다고 생각하기보다 세미콜론이 삽입된다고 생각하게 만든다는 것이다. 이 문제를 우회적으로 생각하게 만드는 방식이다. 하지만 어쨌든 현실이 그렇다. 여기서 강조하고 싶은 부분이 있다. 세미콜론은 _lexer_가 삽입한다. 그 이유는 자동 세미콜론 삽입 규칙을 매우 단순하게 유지할 수 있기 때문이다.
Go의 lexer는 다음 토큰들이 줄바꿈이나 } 바로 앞에 나타나면 그 뒤에 세미콜론을 삽입한다:
break, continue, fallthrough, return, ++, --, ) 혹은 }.충분히 단순하다! 서두의 예제로 가 보자:
x := 4
y := 2 * x
- 3
각 줄이 숫자로 끝나므로 lexer가 세미콜론을 삽입한다:
x := 4;
y := 2 * x;
- 3;
Python처럼 오류가 나기 쉬워 보인다! 하지만 실행해 보면 Go는 좋은 놀라움을 준다. 오류가 난다(경고가 아니라!):
-3 (untyped int constant) is not used
즉, 실수를 막는 가드레일이 있다. -3을 더 복잡한 표현식으로 바꿔도, 보통은 실수로 생길 수 있는 "unused value"에 대해 오류가 난다. 좋다!
This post는 오류가 나지 않으면서 줄바꿈이 동작을 바꾸는 예를 준다. 먼저 약간의 준비가 필요하다:
func g() int {
return 1
}
func f() func(int) {
return func(n int) {
fmt.Println("Inner func called")
}
}
그리고 다음 스니펫들은 의미가 다르다:
f()
(g())
f()(g())
솔직히 이건 크게 걱정되지 않는다. 모호함으로 보이려면 꽤 난해한 코드가 필요해 보인다.
이제 세미콜론 삽입이 전적으로 lexer에서 이뤄진다는 점을 다시 떠올리자. 그러면 세미콜론이 예상치 못한 위치에 삽입되기도 한다:
if x // <- semicolon inserted here
{
...
}
foo(
x // <- semicolon inserted here
)
둘 다 parse 오류가 난다. 해결책은 Go의 사실상 필수 포맷팅 스타일을 따르는 것이다:
if x {
...
}
foo(x)
// or
foo(
x,
)
공정하긴 하지만, 조금 지나치게 규범적이라는 느낌도 있다. 나는 이런 포맷 선택을 좋아하지만, "잘못된 스타일"도 문법적으로는 유효하고 포매터가 고칠 수 있으면 더 좋겠다. 지금의 Go에서는 포매터도 이런 잘못된 스니펫에서 오류를 낸다. 이런 엄격함은 특히 Java처럼 중괄호를 다음 줄에 두는 경우가 흔한 언어에서 온 사람들에게 lead to confusion for newcomers를 가끔 일으키는 듯하다.
결론적으로 Go의 접근은 단순하지만, 내 생각엔 친절하진 않다. 일부 unused value를 금지함으로써 보완하긴 하지만, 내가 Go를 충분히 잘 쓰지 못해서 이게 모든 모호한 경우를 덮는지 평가하긴 어렵다.
Sources:
내가 이해한 바로는, Kotlin은 Python이나 Go처럼 "줄바꿈이 두 문을 나누는" 단순 규칙을 갖고 있지 않다. 대신 줄바꿈을 문법의 명시적인 일부로 만든다. 즉, 각 구문에서 줄바꿈을 허용할 곳을 명시적으로 선택한다. BNF 비슷한 표기는 생략하겠지만, 요지는 다음으로 보인다:
문은 하나 이상의 줄바꿈 또는 ;로 분리된다.
줄 끝에서 어떤 구문이 명백히 미완성이라면, 다음 줄로 이어지는 것이 허용된다.
(함수 호출 같은) 구분자로 감싼 구문은 그 내부에서 줄바꿈을 허용한다.
(, [ 또는 { 앞에는 줄바꿈이 허용되지 않는다.
이항 연산자는 두 부류로 나뉘는 듯하다:
&&, ||, ?:, as, as?, ., .?는 연산자 양쪽에 줄바꿈을 허용하고,전위 단항 연산자는 자기 자신 뒤에 줄바꿈을 허용한다.
줄바꿈 처리를 문법에 "구워 넣는" 접근은 언어 설계자에게 큰 통제력을 주지만, 단순성과 투명성을 희생한다. Go의 정반대 같은 느낌이다. 규칙이 꽤 미묘해질 수 있고, 이를 명확히 설명한 글을 찾기 어렵다.
내가 할 수 있는 최선의 요약은 이렇다: 문법상 모호함이 없을 때에만 표현식이 다음 줄로 이어질 수 있다.
그 이론 뒤에, 예시를 시험해 보자:
val x = 4
val y = 2 * x
- 3
print(y)
결과는 8이고 unused value 경고가 뜬다. -, +를 비롯한 많은 중위 연산자들이 연산자 _뒤_에만 줄바꿈을 허용하기 때문에 자연스럽다. 반면 논리 연산자 &&와 ||는 양쪽 모두에서 줄바꿈을 허용한다.
// This is one expression:
val y = false
|| true
// This is two expressions:
val y = 1
+ 2
"모호하지 않으면 이어진다" 접근이 곤란해지는 또 다른 경우는, 매우 비슷한 연산자들이 서로 다른 규칙을 가질 때다. Kotlin에는 각각 클래스의 메서드와 필드에 접근하기 위한 ::와 . 연산자가 있다. 이 둘 중 .는 양쪽에서 줄바꿈을 허용하지만, ::는 그렇지 않다. ::가 callable reference 표현식의 유효한 시작이기도 하기 때문이다.
val x = foo
.bar // one expression!
val y = baz
::quux // two expressions!
줄바꿈이 문법의 명시적 일부라서 어떤 곳에서는 분명히 금지되므로, +가 문법상 연산자 뒤에만 줄바꿈을 허용하니 다음은 오류가 날 거라고 예상했다:
val y = (
1
+ 2
)
그런데 동작한다! 이런 동작을 추가한 것이 타당하다고는 생각하지만, 명세에서 이 동작의 흔적을 찾지 못했다. 누군가 어디에 문서화되어 있는지 보여줄 수 있다면 정말 보고 싶다!
이 구현에서 내가 받는 인상은, Kotlin 설계자들이 필요한 규칙과 예외가 얼마나 많아지든 동작이 직관적이도록 아주 노력한다는 것이다. 사람들이 실제로 문제를 겪지 않는다면, 완전히 이해할 필요도 없다는 관점일 수 있다. 완전히 동의하진 않지만, 어느 정도는 이해할 만한 입장이다.
This Stack Overflow answer도 그 정서를 반영한다:
The rule is: Don't worry about this and don't use semicolons at all [...]. The compiler will tell you when you get it wrong, guaranteed. Even if you accidentally add an extra semicolon the syntax highlighting will show you it is unnecessary with a warning of "redundant semicolon".
이 접근은 "걱정하지 마, IDE가 고쳐줄 거야"로 요약할 수 있을 것 같다. 언어를 만든 회사가 IDE도 만든다면 그럴듯하긴 하다. 다만 커뮤니티의 합의가 정말 그렇다면, 그들은 꽤 잘 해낸 셈이다!
또 다른 잠재적 문제는, 이렇게 복잡한 규칙들이 Kotlin용 커스텀 파서를 작성하기 어렵게 만들 수 있다는 점이다. 예를 들어 tree-sitter grammar를 유지보수해야 하는 사람은 되고 싶지 않다.
Sources:
아직 나오지 않은, 다소 명백한 접근이 하나 있다. 줄바꿈을 무시하고 가능한 한 끝까지 파싱하는 것이다. Swift는 그 접근을 택했고, 이유를 이해하기 어렵지 않다:
let x = 4
let y = 2 * x
- 3
print(y)
기대대로 5가 출력된다. 단점은 이것도 5를 출력한다는 점이다:
let x = 4
let y = 2 * x
- 3
print(y)
하지만 "이 언어는 공백이 의미를 갖지 않는다"라는 규칙만 있다면 그리 나쁘지 않다. 사람들이 기억할 수 있는 규칙이다. 흥미롭게도 Swift는 실수를 막기 위해 some significant whitespace를 갖고 있다. 예를 들어 한 줄에 여러 문을 쓰는 것은 허용되지 않는다:
var y = 0
let x = 4 y = 4 // error!
문법 명세에서는 이걸 무시하기로 한 듯하지만, 컴파일러에는 들어 있다.
이 접근에서 내가 찾을 수 있는 가장 혼란스러운 예시는 단항과 이항 둘 다 될 수 있는 기호들 주변이다(영원한 숙적). 다음 스니펫은 8을 출력한다:
let x = 4
let y = 2 * x
-3
print(y)
왜일까? Swift는 연산자를 파싱하는 특별 규칙이 있다. 연산자 양쪽에 공백이 있거나 양쪽 모두 공백이 없으면 중위(infix)로 파싱한다. 왼쪽에만 공백이 있으면 전위(prefix) 연산자다. 마지막으로 오른쪽에만 공백이 있으면 후위(postfix) 연산자다. 그래서 다음도 두 문으로 파싱된다:
let y = 2
-foo()
Swift 설계자들은 이 문제를 알고 있고(당연히), 따라서 unused value에 대한 경고를 내는데 위 예시에서 그 경고가 발생한다. 이는 대부분의 오류 사례를 잡아낼 것이다.
또 다른 조정으로, 함수 호출의 괄호는 함수 이름과 같은 줄에 있어야 한다. 그렇지 않으면 첫 줄에서 표현식이 끝난다. 예를 들어 아래 스니펫은 두 줄로 파싱된다. (가 줄 시작에 있는지 확인하고, 그렇다면 파싱을 계속하지 않는다. [도 마찬가지다. 꽤 좋은 규칙이다! JavaScript 섹션을 보면 어떤 언어가 이걸 어떻게 망칠 수 있는지 볼 수 있다.
let y = x
(1)
문법 오류에 대한 오류 보고도 논의할 가치가 있다. Swift는 문법이 올바르지 않을 때 문이 어디에서 끝나야 하는지 쉽게 추측할 수 없다.
let x = 4
let y = ( 2 * x
print(y)
이 스니펫은 )가 빠져서 명백히 잘못됐지만, Swift는 대신 ,가 빠졌다고 불평하고, 선언 전에 y를 사용했다며 _circular reference_를 말한다. 문이 끝날 위치를 알 수 없기 때문이다. 공정하게 말하면, 이에 대해 불평하는 1 comment만 찾았으니 큰 문제는 아닐 수도 있다. 내가 Swift를 충분히 써본 게 아니라 판단하기 어렵다.
나는 이 접근이 꽤 마음에 든다. 직관적이면서도 이해하고 디버깅하기 쉽다. 명시적인 세미콜론이 있는 언어에 비해 오류 메시지가 조금 손해를 볼 수는 있지만, 대신 "missing semicolon" 오류도 사라지니 일종의 트레이드오프다.
Sources:
JavaScript는 Automatic Semicolon Insertion에 나쁜 평판을 준 언어로 보인다. 규칙이 꽤 복잡하지만, 다행히 훌륭한 MDN article이 있다.
세미콜론이 삽입되는 중요한 경우는 세 가지다:
a. 이전 토큰과 최소 한 개의 줄바꿈으로 분리되어 있거나,
b. 토큰이 }일 때.
입력의 끝에 도달했는데, 그 상태가 문법상 허용되지 않을 때.
return, break, continue 뒤 같은 특정 표현식에서 줄바꿈을 만났을 때.
이게 전부는 아니라는 점에 주의하라! 예를 들어 for 문의 head에서는 세미콜론이 삽입되지 않는다거나, 빈 표현식을 만들어 버리는 위치에서는 삽입되지 않는다는 등 많은 예외가 있다.
어쨌든, 이것은 우리의 예시가 한 줄로 파싱된다는 뜻이다:
const y = 2 * x
- 3
// is parsed as
const y = 2 * x
- 3;
이 규칙의 복잡성은 그 자체로 문제다. 기억하기 어렵기 때문이다. 더 최악인 점은, 첫 번째 규칙이 오직 문법적으로 잘못된 경우에만 발동한다는 것이다. MDN 글에는 다음과 같은 실패 사례가 가득하다. 아래 스니펫들은 둘 다 한 줄로 파싱된다:
const a = 1 // <- no semicolon inserted!
(1).toString()
const b = 1 // <- and also not here
[1, 2, 3].forEach(console.log)
JS에서 세미콜론 없이 코딩하려면, 연속된 줄들이 붙었을 때도 문법적으로 유효한지 계속 생각해야 한다. 또는 다음 같은 수많은 규칙을 배워야 한다:
return, break 등의 피연산자를 다음 줄에 두지 말 것.(, [, ```, +, -, / 같은 것으로 시작한다면 앞에 세미콜론을 붙이거나 이전 줄을 세미콜론으로 끝낼 것.그러니 많은 사람들이 JS에서 그냥 세미콜론을 쓰기로 하는 것도 놀랍지 않다. 예를 들어 JavaScript: The Good Parts에는 이런 문장이 있다:
JavaScript has a mechanism that tries to correct faulty programs by automatically inserting semicolons. Do not depend on this. It can mask more serious errors.
결론적으로 JS는 세미콜론 없이 쓸 수는 있지만, 많은 사람들이 항상 세미콜론을 추가하라고 권하는 현실은 꽤 치명적이다. 이 글의 다른 언어들에서는 그런 분위기를 보지 못했다. 이는 이 기능이 득보다 실이 크다는 뜻이다. 이 기능은 너무 복잡하고, 견고하지도 못하다. 솔직히 말해, 이 기능은 재앙이다.
Sources:
Gleam의 접근은 Swift와 매우 비슷하다. 공백과 상관없이 표현식이 자연스럽게 끝날 때까지 파싱한다. Swift는 몇 가지 예외가 있었는데, Gleam은 어떤지 살펴보자.
먼저 반복 예시를 보자:
let y = 2 * x
- 3
예상대로 한 표현식으로 파싱된다. 그런데 공백 하나를 지우면 바뀐다:
let y = 2 * y
-3
Swift와 비슷하게, Gleam은 -3이 앞에 공백이 있으면 하나의 토큰으로 파싱하고, 그렇지 않으면 이항 연산자로 파싱하는 듯하다. 출처를 찾지 못해서 세부사항은 틀렸을 수도 있다.
공백을 무시하고 뭐든 파싱하는 Gleam의 접근은 몇 가지 이상한 결과를 낳는다. 예를 들어 다음은 허용되며 2개의 표현식으로 파싱된다:
pub fn main() {
1 + 1 1 + 1
}
개인적으로 내가 Gleam을 설계했다면 거기에 줄바꿈을 요구했을 것이다. 하지만 기술적으로는 모호하지 않다. Gleam 포매터는 표현식을 별도 줄로 배치할 것이고, Gleam은 unused value 경고도 내므로, 곧 뭔가 이상하다는 걸 알아차릴 것이다.
다음은 한 표현식, 즉 함수 호출로 파싱된다:
pub fn main() {
foo
(1 + 1)
}
Gleam을 써본 사람이라면 "그건 모호하지 않아!"라고 외치고 있을지도 모른다. 맞다. 함수 호출밖에 될 수 없다. Gleam은 표현식 그룹핑에 {}를 쓰기 때문이다. 그래서 {}를 쓰면 더 이상 함수 호출이 아니다:
pub fn main() {
foo
{ 1 + 1 }
}
또 하나의 훌륭한 모호성 방지로, Gleam에는 []로 리스트 인덱싱을 하는 기능이 없다. 그래서 이것도 두 표현식으로 파싱된다:
pub fn main() {
foo
[ 1 + 1 ]
}
흥미롭게도 Gleam은 Swift 같은 가드레일이 없다. 대신 문법 자체가 매우 비모호해서 그게 가능하다. 대단히 인상적인 언어 설계다. 규칙도 이해하기 쉬워서, 내게는 꽤 좋은 구현처럼 보인다.
Sources:
가능한 한 끝까지 파싱하는 언어 얘기가 나왔으니, Lua도 그렇다! book에는 이렇게 적혀 있다:
A semicolon may optionally follow any statement. Usually, I use semicolons only to separate two or more statements written in the same line, but this is just a convention. Line breaks play no role in Lua's syntax[.]
즉 Gleam처럼 동작한다! 차이점은 Lua는 []로 인덱싱을 하고 ()로 표현식을 그룹핑한다는 것이다. 아래 예시는 단일 문으로 파싱되는 것을 막기 위해 세미콜론이 필요하다:
(function() end)(); -- semicolon is required here
(function() end)()
더 문제가 되는 경우가 있을 수도 있지만, 나는 Lua 경험이 부족해서 찾지 못하겠다.
Sources:
앞에서 어떤 언어들은 더 읽으면 문법적으로 잘못될 때 세미콜론을 삽입한다고 봤다. R은 그 반대에 가까운 접근을 취한다. 문법이 허용하는 경우 줄바꿈에서 세미콜론을 삽입한다. R Language Definition의 공식 설명은 다음과 같다:
Newlines have a function which is a combination of token separator and expression terminator. If an expression can terminate at the end of the line the parser will assume it does so, otherwise the newline is treated as whitespace.
이 규칙의 예외는 하나인데, else 키워드는 별도 줄에 올 수 있다.
이 접근은 어느 정도 Python을 떠올리게 한다. 하지만 R은 표현식이 미완성이면 다음 줄로 이어질 수 있다. 반복 예시는 x 뒤에서 표현식이 끝날 수 있으므로 두 표현식으로 파싱된다:
y = 2 * x
- 3
하지만 약간 수정하면 하나의 표현식으로 파싱된다:
y = 2 * x -
3
결과적으로, 다음 표현식이 이전 표현식의 일부로 파싱될까 걱정할 일이 거의 없다. 괄호나 연산자 꼬리처럼 명시적인 경우에만 결합되기 때문이다. 단점으로는, 나는 보통 연산자를 다음 줄의 시작에 두는 편을 선호하는데, 그러려면 (Python처럼) 괄호로 감싸야 한다.
그래도 꽤 좋은 접근처럼 보인다. 줄바꿈이 어느 정도 의미를 갖고, 혼란스럽지 않게 느껴진다.
Sources:
세미콜론이 유명하게 없는 언어로 Ruby를 빼놓을 수 없다. R과 매우 비슷한 접근을 취하지만, 또 — 이제는 좀 익숙한 패턴이지만 — 완전히 같지는 않다. R처럼 줄로 문을 나누되, 표현식이 미완성이라면 이어지도록 허용한다. 그래서 R의 예시를 거의 그대로 가져올 수 있다:
# 2 expressions
y = 2 * x
- 3
# 1 expression
y = 2 * x -
3
하지만 Ruby는 몇 가지 추가 트릭이 있다. 첫째, Python처럼 줄 끝에 \를 두어 다음 줄로 명시적으로 이어갈 수 있다. 둘째, . 또는 && 또는 ||로 시작하는 줄은 이전 줄의 연속이라는 특별 규칙이 있다. 메서드 체이닝과 논리 체인을 가능하게 하려는 것이다.
File.read('test.txt')
.strip("\n")
.split("\t")
.sort
File.empty?('test.txt')
|| File.size('test.txt') < 10
|| File.read('test.txt').strip.empty?
나는 이게 약간 혼란스럽다. 어떤 연산자는 다음 문을 시작할 수 있는데 어떤 연산자는 안 된다는 게 이상하기 때문이다. 그래도 3가지 예외만 기억하면 되니 나쁘진 않다. 그래서 전체적으로 꽤 좋아 보인다!
Sources:
Julia 문법이 어떻게 동작하는지에 대한 문서를 찾기가 좀 어려워서, 파싱 코드를 살펴봤다. 그래서 의도를 조금 추측해야 한다.
내가 시도한 것들은 다음과 같다:
b = 3
- 4
# -> 3
c = 3 -
4
# -> -1
d = ( 3
- 4)
# -> -1
줄바꿈이 문을 이어주는지 여부가 표현식의 종류에 따라 달라지는 듯하다. 하지만 대체로, 합법적이라면 여러 줄로 나누는 것을 선호하는 것 같다. 파서에서는 줄바꿈이 정말로 구분자로 취급된다. 그런 의미에서, Python이나 R처럼 과학 커뮤니티에서 많이 쓰이는 다른 언어들과 닮아 있다.
Julia 문법 문서를 어디서 찾을 수 있는지 아는 사람이 있다면 알려 달라!
Sources:
이 글을 쓰는 동안, Odin의 창시자 GingerBill이 Odin 접근을 설명하는 blog post를 공개했다. 내가 특히 흥미롭게 본 것은, 세미콜론을 선택 사항으로 만든 이유다:
There were two reasons I made them optional:
- To make the grammar consistent, coherent, and simpler
- To honestly shut up these kinds of bizarre people
이 기능 자체에는 별 관심이 없었던 것처럼 보인다. 좋은 점은 이 글이 Odin 접근에 대한 논리를 제시한다는 것이다. 그는 이를 Python과 Go의 혼합이라고 설명하는데, lexer가 세미콜론 삽입을 수행하되 (), {}, [] 안에서는 수행하지 않는다는 것이다.
그가 제시한 또 다른 예외는, Odin이 중괄호를 다음 줄에 둘 수 있도록 몇 가지 예외를 둔다는 점이다:
a_type :: proc()
a_procedure_declaration :: proc() {
}
another_procedure_declaration :: proc()
{
}
another_type :: proc() // note the extra newline separating the signature from a `{`
{ // this is just a block
}
어떤 의미에서는, 특정 코딩 스타일을 강제하는 Go와 반대로, 자기들 스타일 말고 다른 스타일도 허용하려고 애쓰는 것처럼 보인다. 이 규칙은 문법이 다소 "과적"되었을 가능성을 암시한다. 즉, 서로 다른 개념에 매우 비슷한 문법을 쓰는 것이다. 하지만 분명 그럴 만한 이유가 있었을 것이다.
Sources:
여기, 내가 실제로 쓰이는 것을 보지 못했는데 타당한지 궁금한 아이디어가 있다.
들여쓰기를 고려하는 언어는 Python이 유일해 보이는데, 그것도 실수를 제한하기 위해서뿐이다. 나는 오히려 "들여쓰기된 줄만" 이전 표현식의 일부로 간주하는 규칙을 적용한 언어를 보고 싶다.
x = 3
- 3 # two expressions!
x = 3
- 3 # one expression!
이건 꽤 직관적으로 느껴진다. Python의 \ 줄 결합을 대체할 수도 있을 것 같다. 물론 문제는 이제 들여쓰기가 항상 정확해야 하고, 많은 개발자들(나 포함)은 들여쓰기는 그냥 포매터가 처리해 주길 좋아한다는 점이다. 어쨌든, 세미콜론이 선택인 언어에서 고려할 만한 흥미로운 lint가 될 수는 있겠다.
끝까지 왔다! 이 문서를 요약하는 가장 좋은 방법은 언어들을 묶는 것이라고 생각한다:
줄바꿈에서 문을 분리하되, 예외가 있는 경우
다음 줄로 문을 이어가되, 그게 문법적으로 불가능할 때만 멈추는 경우
lexer가 세미콜론을 삽입하는 경우
파싱에서 공백을 고려하지 않는 경우
Note: 이 분류는 완벽하지 않다. 어떤 언어는 여러 분류에 동시에 들어간다고 주장할 수도 있다.
다른 분류도 만들 수 있다. 예를 들어 Python, Ruby, R, Julia, Odin은 파싱이 _보수적_이라고 부를 수 있다. 보통 줄바꿈에서 파싱을 멈춘다. 반면 Lua, Gleam, Swift는 더 _탐욕적_이다. 가능한 한 줄바꿈을 넘어 계속 파싱한다.
또 다른 구분은 구현 방식이다. JavaScript, Go, Odin은 세미콜론 삽입의 적어도 일부를 lexer에 구현하는 반면, 다른 많은 언어는 이를 파서의 일부로 만든다.
마지막으로 흥미로운 범주는 Lua와 Gleam처럼 공백에 전혀 민감하지 않은 언어들이다. Swift는 여기에 꽤 가까워 보이지만, 실제로는 공백에 민감한 규칙이 몇 가지 있었다.
이 주제는 내가 예상했던 것보다 훨씬 복잡했다! 나는 어떤 접근은 다른 것보다 더 마음에 들지만, 모든 언어가 같은 해법을 써야 하는 것은 아니다. 문법의 다른 부분들이 다를 수 있고, 그에 따라 고려해야 할 문제가 달라질 수 있기 때문이다.
그럼에도 불구하고, 여기서는 내 의견을 말해야 할 것 같으니(당연히 당신은 동의하지 않을 수도 있지만), 내가 사용할 만한 가이드라인을 적어 보겠다:
동의하는가? 무엇이 최선이라고 생각하는가? 중요한 언어를 빼먹었을까? 더 나은 구현을 위한 멋진 아이디어가 있는가? this post on Mastodon에 답글로 알려 달라!
이 글의 초안들을 교정해 준 Thijs Vromen, waffle, Anne Stijns에게 감사한다. 모든 실수는 내 책임이다. 수정 사항은 terts.diepraam@gmail.com 또는 on Mastodon으로 보낼 수 있다.
이 글을 쓰는 동안 LLM은 사용하지 않았다. 정보 수집에도, 글쓰기에도 사용하지 않았다.