Rye 0.2는 연산자 평가 순서를 좌→우로 바꾸고, 새로운 단어 타입 dot-word를 도입하며, 타입 생성자와 파일-URI 메서드 명명, failure 생성자 등을 더 일관되게 다듬는다.
TL;DR: 새로운 좌→우 평가 순서, 중요한 새 단어 타입 [dot-word], 더 단순한 생성자.
March 4, 2026·Janko
목차
Rye 0.2가 출시되었습니다. 이번 Rye는 평가 규칙과 용어에 큰 변화를 가져오므로 Rye 0.1.* 버전들에서 바로 점프했습니다.
이번 변경은 특히 연산자와 관련된 평가 순서에 얽힌 큰 “함정(footgun)” 동작을 제거합니다. 새로운 단어 타입 dot-word를 도입하고, (기존) op-word 동작도 어느 정도 바꿉니다.
대부분의 기존 코드는 변경 없이 계속 동작할 것입니다. 일반적으로 우리가 바로 그 함정인 패턴을 사용하지 않았기 때문입니다. 하지만 그것은 언제나 거기에 있었고, 안전장치가 해제된 채 준비되어 있었습니다.
그 함정은 이렇습니다. op-word가 우결합(right associative)이었고 우→좌로 적용되었는데, 이는 특히 수학 연산자에서 매우 직관에 반했습니다.
; Rye <= v0.1.*
12 - 6 - 4 ; returned 10
11 + 2 > 12 ; error: _+ doesn't accept boolean
; Rye >= v0.2.*
12 - 6 - 4 ; returns 2
11 + 2 > 12 ; returns true
Rye는 Rebol 계열에서 왔고, 그곳에서는 일관된 좌→우 연산자 평가가 표준입니다. C 스타일의 연산자 우선순위 규칙이 아니라요.
가끔 어색한 코드를 방어하기 위해, 저는 셔닝 야드(shunting yard) 알고리즘을 사용해 올바른 수학 연산자 우선순위를 제공하면서도 빠른 평가와 최소한의 “잉크”로 _인라인_에서 쓸 수 있는 새로운 mth 방언(dialect)을 작업하고 있었습니다.
if mth { 3 + 3 * 2 = 9 } { print "Yo" } ; prints Yo
구현은 되었고, 저는 이게 얼마나 멋진지에 대한 블로그 글을 쓰고 있었습니다. 다른 대안은 없다는 주장까지 하려 했죠.
하지만 글을 쓰던 중에 제 논리를 의심하기 시작했습니다. 저는 작은 패치를 제공하고 있을 뿐인데, 기본값은 항상 그 자리에 남아 있어 새로운 또는 조금만 부주의한 Rye 사용자가 걸려 넘어지길 기다리고 있다는 점이 걸렸습니다.
저는 기본을 다시 평가했고, 그 결과는 수정 그 이상이었습니다. 이제 저는 이것이 Rye의 평가 메커니즘을 한 바퀴 완성시키는 잃어버린 고리일지도 모른다고 생각합니다.
(mth 방언은 여전히 쓸모가 있으므로 Rye에서 사용할 수 있습니다)
이전에 _op-word_라고 불렀던 단어들(앞에 점이 붙는 .word)에서 +, *, … 같은 연산자를 뺀 것들은 이제 dot-word라고 부릅니다. (이제는) 첫 번째 인수를 왼쪽에서 가져오며(이전 op-word처럼), 좌결합(left associative)이고 좌→우로 평가합니다.
99 .inc ; returns 100
"hello" .upper ; returns "HELLO"
"hello world" .replace "world" "mars" ; returns "hello mars"
2 .inc .string .concat " bears" ; returns "3 bears"
3 .+ 4 .* 5 ; returns 35
; prefix operators with dots and turn them into dot-words
정신 모델: dot-word는 왼쪽 피연산자에 첫 인수를 (단단하게) 고정(pin)하여, 일종의 원자(atoms)(가장 작은 표현식 덩어리)를 만듭니다.
Op-word는 이제 연산자 +, *, ->, ++, … 와 꺾쇠 괄호가 있는 단어 <concat>를 말합니다. 그리고 이들도 좌→우로 평가합니다.
12 - 6 - 4 ; returns 2
11 + 2 > 12 ; returns true
3 * 4 - 5 <string> <concat> " ravens"
; returns "7 ravens"
정신 모델: op-word는 값들 또는 앞서 말한 원자들 사이의 **다리(bridges)**에 더 가깝습니다. 같은 동작, 다른 우선순위입니다.
Rye가 처음이라면 — 그리고 누가 아니겠어요 :) 함수의 op-word 성질은 어떤 단어나 함수 자체에 묶여 있지 않습니다. 모든 함수(그리고 Rye에서 모든 활성 요소는 함수입니다:
print``if``fn…)는 op-word 형식인<print>로 입력하면 op-word가 됩니다. 연산자는 기본적으로 op-word일 뿐입니다.
.word) - 왼쪽 값에 **고정(pin)**되어 원자를 만들고, 좌→우+, -, <word>) - 두 값/원자를 **연결(bridge)**하거나 다른 bridge에 연결, 좌→우word) - 전면(front), 오른쪽에서 인수를 가져오며 pin과 bridge를 감쌈|word) - 벽(wall) - 왼쪽이 완전히 평가되길 기다린 뒤 결과를 받음먼저 dot-words, 그다음 op-words:
10 .dec ; returns 9
10 .inc .math/is-prime ; returns true
12 - 6 - 4 ; returns 2
12 + 3 = 15 ; returns true
10 .inc - 10 .dec .dec ; returns 3
"AB" .lower <concat> "bc" .upper ; returns abBC
4 .+ 2 * 2 .+ 3 ; returns 30
그다음 words:
print 12 - 6 - 4 ; prints 2
if 12 + 3 = 15 { print "Yo" } ; prints Yo
372 .string <concat> upper "xp" ; returns 372XP
pipe-words는 여전히 단단한 벽입니다:
inc 10 * 10 ; returns 101
inc 10 |* 10 ; returns 110
print 12 - 6 |- 4 ; prints 6, returns 2
11 .print + 22 <print> + 33 |print
; Prints:
; 11
; 33
; 66
더 깊이 다루는 글이 나중에 올라올 예정입니다 — 아마 “The Geometry of a Language” 같은 이름이 될 텐데요 — 이 새로운 평가 메커니즘의 배경 논리, 다른 언어와의 비교, 그리고 이 모델이 왜 코드를 더 시각적으로 정확하고, 국소적이며, 유연하게 만들 수 있는지 더 깊게 파고들 것입니다.
값 생성자는 Rye에서 늘 타입 이름 그 자체였습니다. context, dict, list, failure, vector. 하지만 to-<type> 같은 것도 있었는데, 예를 들면 to-integer, to-string처럼 해당 타입으로 변환하는 단어들입니다. 그런데 타입을 “생성”하는 것과 타입으로 “변환”하는 것의 차이가 그리 명확하지 않습니다. 생성자도 입력으로부터 해당 타입으로 변환을 했기 때문입니다. 우리는 이를 하나로 합치고 모든 “to-”를 제거하여 더 일관되고 덜 시끄럽게 만들려고 합니다.
| Old | New |
|---|---|
to-integer | integer |
to-decimal | decimal |
to-string | string |
to-uri | uri |
to-file | file |
to-char | char |
to-block | block |
to-word | word |
integer "42" ; 42
string 123 ; "123"
file "data.txt" ; %data.txt
to-context가 context 함수로 합쳐졌기 때문에, 이제는 블록(평가됨)과 dict(직접 변환됨) 둘 다를 받습니다:
; from block - expressions are evaluated
c: context { name: "Alice" age: 30 adult: age >= 18 }
c/adult ; true
; from dict - values used directly
data: dict { "name" "Bob" "age" 25 }
c: context data
c/name ; "Bob"
File-URI 메서드에서 중복적인 File- 접두사가 제거되었습니다:
| Old | New |
|---|---|
File-ext? | Ext? |
Filename? | Name? |
%path/to/document.pdf .Ext? ; ".pdf"
%path/to/document.pdf .Name? ; "document.pdf"
%path/to/document.pdf .Stem? ; "document"
%path/to/document.pdf .Dir? ; "path/to"
failure는 이제 인수를 어떤 순서로든 받을 수 있습니다. 타입, 상태 코드, 메시지는 위치가 아니라 타입으로 식별됩니다:
failure { 'validation 400 "Invalid input" }
failure { 400 'validation "Invalid input" }
fail { "Invalid input" 'validation 400 }
; optional parts can be omitted
failure { 'not-found "Resource missing" }
fail { 500 "Server error" }
모든 failure 생성에 영향을 줍니다:
1 / 0 |check { "calculation error" 501 }
; Error(501): calculation error
; Error: Can't divide by Zero. In builtin _/
age: 20
age >= 21 |ensure { 403 "Must be 21 or above" }
; Error(403): Must be 21 or above
Rye 0.1.x에서 Rye 스크립트를 업그레이드한다면:
to-* 생성자를 교체하세요: to-string ->string, to-integer ->integer, 등.File-ext? ->Ext?, Filename? ->Name?."hello" .upper .trim .split " " 는 기대대로 좌→우로 평가됩니다.늘 그렇듯 GitHub로 피드백을 환영합니다. 또는 ryelang.org에서 직접 사용해 보세요. 그라인더에서 바로 나온 글을 보려면 r/ryelang을 방문하세요.