Python과 달리 Rye·REBOL·Red처럼 if, for, while, fn, var가 일반 함수일 때 생기는 일관성, 유연성, 확장성의 이점과 실제 코드 예시, 그리고 트레이드오프를 살펴봅니다.
Python에서 if x > 5: print("big")를 쓰면, 이는 언어에 박혀 있는 특별한 문법을 사용하는 것입니다. if의 동작을 바꿀 수 없고, 합성하거나 파이핑하거나 부분 적용할 수 없습니다. if를 다른 함수의 인수로 넘길 수도 없죠.
그런데 만약 그게 가능하다면? if, for, while, 심지어 fn과 var까지도 그저 평범한 함수라면?
REBOL, Red, 그리고 Rye 같은 언어에선 실제로 그렇습니다.
일관성. 대부분의 언어는 제어 구조를 일반 규칙의 예외(특수 양식)로 취급합니다. Rye와 REBOL 같은 언어에선 이들이 다른 모든 것과 동일한 패턴을 따르는 평범한 함수입니다.
유연성. 함수는 합성, 전달, 결합이 가능합니다. 제어 구조가 함수가 되면 그 능력을 그대로 물려받습니다.
확장성. if와 for가 그저 함수라면, 특정 목적에 특화된 자신만의 버전을 만들 수 있습니다. 언어의 어떤 부분도 금지 구역이 아닙니다.
이게 실제로 어떻게 보이는지 살펴보겠습니다.
다음은 Python의 조건문입니다:
temperature = 36
# 표현식을 평가해서 출력할 수 있음
print(temperature > 30)
# 출력: True
# 코드 블록 자체를 바로 출력하거나 평가할 순 없음
# 문자열로 되돌리는 편법 말고는 마땅치 않음
# 표준 조건문 - 특별한 문법
if temperature > 30:
print("It's hot!")
# 출력: It's hot!
Rye와 비교해 보세요:
temperature: 36
; 표현식을 평가하고 결과를 출력
print temperature > 30
; 출력: true
; Rye에선 블록이 데이터 - 블록을 출력할 수 있음
print { print "It's hot!" }
; 출력: print "It's hot!"
; 'do' 함수는 Rye 값/코드 블록을 평가함
do { print "It's hot!" }
; 출력: It's hot!
; 그리고 조건문 - 이것도 그저 함수
if temperature > 30 {
print "It's hot!"
}
; 출력: It's hot!
마지막 if를 보세요. 특별한 문법이 아니라 함수 호출입니다. print와 do가 인수 하나를 받는 함수인 반면, if 함수는 두 인수를 받습니다:
“블록을 인수로 넘기면 바로 실행되지 않나요?”라는 의문이 생길 수 있습니다. 핵심은 Rye에서 코드 블록 { ... }은 값이라는 점입니다. 명시적으로 지시하지 않으면 평가되지 않습니다. if 함수는 블록을 데이터로 받아, 조건에 따라 그것을 평가할지 말지를 결정합니다.
코드가 데이터이면, 제어 흐름은 특별할 필요가 없습니다.
Python에선 각 언어 기능마다 제각각의 문법이 있습니다:
# 조건문 - 키워드와 콜론
if x == 5:
print("five")
# 루프
for i in range(10):
print(i)
# 반복
for item in ["milk", "bread", "pickles"]:
print(item)
# 함수 - def 키워드, 괄호, 콜론, 들여쓰기
def add(a, b):
return a + b
Rye에선 하나의 패턴이 어디에나 적용됩니다:
; 조건문 - 불리언과 블록을 받는 함수
if x = 5 { print "five" }
; 카운팅 루프 - 정수와 블록을 받는 함수
loop 10 { .print }
; 반복 - 컬렉션과 블록을 받는 함수
for { "milk" "bread" "pickles" } { .print }
; 함수 - 인수 목록과 본문 블록을 받는 함수
add: fn { a b } { a + b }
모든 구성 요소가 동일한 형태를 따릅니다: 이름과 그 뒤의 인수들, 그중 몇 개는 코드 블록일 뿐입니다.
이제 “언어 기능”과 “라이브러리 함수” 사이의 의미 있는 구분이 사라집니다.
Python에서 if와 for는 값이 아닌 문(statement)입니다. 하지만 Rye에선 함수이므로, 우선 함수 합성이 가능합니다.
loop either temperature > 32 { 3 } { 1 } { prns "Hot!" }
; 출력: Hot! Hot! Hot!
; 선택적 괄호로 평가 순서를 더 잘 드러낼 수 있음(Lisp 계열처럼)
( loop ( either ( temperature > 32 ) { 3 } { 1 } ) { prns "Hot!" } )
; 출력: Hot! Hot! Hot!
prns는 값 뒤에 공백을 붙여 출력합니다(개행 없음)
Python에서 if 문으로 하려면 여러 줄과 변수 변경이 필요합니다:
repeats = 1
if temperature > 32:
repeats = 3
for _ in range(repeats):
print("Hot!", end='')
# 출력: Hot! Hot! Hot!
다행히 Python에는 또 다른 _특별한 문법_이 있어서, if 키워드가 표현식을 만들 수 있습니다:
for _ in range(3 if temperature > 31 else 1):
print("Hot!", end='')
# 출력: Hot! Hot! Hot!
특별한 문법은 보통 굳어져 있지만, 함수 호출에 인수를 제공하는 방법은 여러 가지가 있습니다.
hot-code: { print "Hot!" }
is-hot: temperature > 30
if is-hot hot-code
; 출력: Hot!
loop 2 hot-code
; 출력: Hot!
; Hot!
Rye의 함수는 첫 번째(또는 두 번째) 인수를 왼쪽에서 파이프로 받을 수 있으므로, “제어 흐름”류의 함수에도 당연히 동일하게 적용됩니다. 자세한 내용은 Meet Rye를 참고하세요.
; 조건을 if로 파이프
temperature > 30 |if { print "Hot!" }
; 출력: Hot!
3 .loop { .prns }
; 출력: 0 1 2
; 컬렉션을 for로 파이프
{ "Hot" "Pockets" } |for { .print }
; 출력: Hot
; Pockets
Python에선 if나 for가 값이 아니라 문법이기 때문에 파이핑할 수 없습니다.
Rye에선 함수에 인수를 적용(apply)할 수 있습니다.
apply ?concat { "Bob" "Odenkirk" }
woof: { print "woof" }
meov: { print "meov" }
animals: [ [ false woof ] [ true meov ] ]
for animals { .apply* ?if }
; 출력: meov
?word - get-word로, 해당 단어에 바인딩된 함수를 실행하지 않고 그 함수를 값으로 반환합니다. word* - 단어 끝의 별표는 함수가 첫 번째가 아니라 왼쪽의 두 번째 인수를 받도록 합니다.
Rye에선 함수의 부분 적용도 가능합니다.
add-five: partial ?_+ [ _ 5 ]
add-five 10
; 반환 15
three-times: partial ?loop [ 3 _ ]
; 다른 함수처럼 사용
three-times { prns "Hey!" }
; Hey! Hey! Hey!
내장 loop에 부분 적용을 해서, 사용자 정의 “제어 구조”인 three-times를 만들었습니다.
함수를 다른 함수의 인수로 전달할 수 있습니다.
여기서는 함수를 받아, 인자를 출력해 주는 동일한 함수를 만들어 반환하는 함수를 정의합니다.
verbosify\2: fn { fnc } {
closure { a b } {
probe a
probe b
fnc a b
}
}
myconcat: verbosify\2 ?concat
myconcat "AAA" "BBB"
; 출력:
; [String: AAA]
; [String: BBB]
; 반환:
; AAABBB
myif: verbosify\2 ?if
myif temperature < 30 { print "cold!" }
; 출력:
; [Boolean: false]
; [Block: ^[Word: print] [String: cold!] ]
제어 흐름이 그저 함수이므로, 내장과 구분되지 않는 자신만의 제어 구조를 작성할 수 있습니다.
; if의 반대가 필요하다면?
unless: fn { condition block } {
if not condition block
}
unless tired {
print "Keep working!"
}
; until 루프가 필요하다면?
until: fn { condition block } {
loop {
r:: do block
if do condition { return r }
}
}
count:: 0
until { count > 5 } {
print count
count:: inc count
}
이 관점을 받아들이면, 어떤 종류의 제어 구조 같은 함수를 가져야 하는지에 딱 잘라 정해진 경계나 한계가 없다는 것을 알게 됩니다. 이런 함수들은 라이브러리 수준에서 불러오면 되고, 특정 라이브러리는 새로운 것들을 제공하거나, 보통은 특수 양식에만 허용되던 형태로 기능을 제공할 수도 있습니다.
기본 함수의 일부지만, 특별한 것으로 볼 수도 있는 예시:
switch password {
"sesame" { "Opening ..." }
"123456" { "Self destructing!" }
}
; 인정합니다, fizz-buzz를 위해 cases 제어 함수를 추가했어요
; 그냥 가능해서요 :P
for range 1 100 { :n
cases " " {
{ n .multiple-of 3 } { "Fizz" }
{ n .multiple-of 5 } { + "Buzz" }
_ { n }
} |prns
}
; 출력: 1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16 ...
Rye의 고차 함수 map filter reduce도 제어 구조처럼 동작하여 코드 블록을 직접 받습니다.
또한 이들은 함수를 받을 수도 있으므로, 고전적인 HOF 방식으로도 쓸 수 있습니다.
{ "anne" "bob" "alan" }
|filter { .start-with "a" }
|map { uppercase } :names
|for { .print }
names .reduce 'acc { .concat acc }
; 고전적인 HOF 패턴도 동작
{ 1 2 3 } .map fn { x } { x + 1 }
심지어 외부 라이브러리도 코드 블록을 직접 활용하는 자체 함수를 가질 수 있습니다.
예를 들어 OpenAI 라이브러리에는 스트림 이벤트마다 분절된 문자열을 블록에 주입해 주는 Chat\stream 함수가 있는데, 이는 for 루프와 비슷합니다.
openai Read %.token
|Chat\stream "A joke of the day?" { .prn }
prn - 줄바꿈이나 공백 없이 문자열을 그대로 출력합니다
Python은 특수 양식을 컴파일 타임에 최적화할 수 있습니다. Rye에선 if가 함수 호출이므로 런타임 오버헤드가 있습니다.
IDE는 Python의 if, for, def를 알고 있고 특화된 지원을 제공합니다. 모든 것이 함수라면, 몇 개의 키워드에 갇히지 않습니다.
Python 같은 언어에선 모든 특별한 문법과 구문마다 각각 최적화와 도구 지원을 마련해야 합니다.
Rye에선 내장 함수와 Rye 함수인 “함수 호출”을 가능한 한 빠르게 만들기만 하면 됩니다. 내장과 함수는 자체적으로 문서화가 가능하므로, 이를 잘 지원하는 도구를 만들면 한 번에 모든 도구 문제를 해결할 수도 있습니다.
더 알아보고 싶다면 Rye, REBOL, Red에서 이러한 아이디어가 실제로 어떻게 작동하는지 확인해 보세요.
그리고 우리의 Github 저장소를 팔로우하세요.