Rye 0.2 is out. We jumped from Rye 0.1.* versions because this Rye brings a major change in evaluation rules and terminology.
Major footgun
The changes remove a major “footgun” behavior related to evaluation order especially in regards to operators. It introduces a new word type dot-word, and somewhat changes (existing) op-word behavior.
Most of existing code will keep on working without changes, because we generally didn’t use the pattern that is the footgun. But it was always there, locked and loaded.
The footgun: op-words were right associative, applied right-to-left, which was particularly counterintuitive with math operators.
; 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 comes from the Rebol family, where consistent left-to-right operator evaluation is the norm - not C-like operator precedence rules.
Started with a patch
To fight the occasional clumsy code, I was working on a new mth dialect that uses the shunting yard algorithm to provide correct mathematical operator priority with fast evaluation and minimal “ink” and could be used inline.
if mth { 3 + 3 * 2 = 9 } { print "Yo" } ; prints Yo
It got implemented, and I was writing a blog-post about how cool it is, trying to make a case that there is no alternative.
But in the middle of it I started questioning my arguments. I am providing a small patch, but the default will always be there, waiting for new or just a little careless users of Rye to trip on it.
I re-evaluated the basics, and what I found was more than a fix, I now think this might be the missing link that completes the circle of Rye’s evaluation mechanisms.
(mth dialect still has its uses, so it is available in Rye)
New: dot-words
Words we called op-words before (.word dot in the front) - minus the operators +, *, … are now called a dot-words. It takes first argument from the left (like op-word before),
but is now left associative, evaluates left-to-right.
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
Mental model: Dot words pin first (tight) to their left operands, creating sort of atoms (smallest expression clumps).
Changed: Op-words
Op-words are now operators +, *, ->, ++, … and words with angle brackets <concat>. And also evaluate left-to-right.
12 - 6 - 4 ; returns 2
11 + 2 > 12 ; returns true
3 * 4 - 5 <string> <concat> " ravens"
; returns "7 ravens"
Mental model: op-words are more like bridges between values or said atoms. Same behaviour, different priority.
If you are new to Rye - and who isn’t :) The op-word-iness of a function is NOT bound to a word or a function. Every function (and in Rye every active element is a function
iffn…) becomes an op-word if you type it in op-word format:<print>. Operators are just op-words by default.
New priority
- Dot-words (
.word) - pin to the left value and form an atom, left-to-right - Op-words (
+,-,<word>) - bridge two values or atoms, or connect to another bridge, left-to-right - Words (
word) - front, takes arguments from the right, wraps pins and bridges - Pipe-words (
|word) - wall - wait for left to fully evaluate, take the result
First dot-words, then 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
Then words:
print 12 - 6 - 4 ; prints 2
if 12 + 3 = 15 { print "Yo" } ; prints Yo
372 .string <concat> upper "xp" ; returns 372XP
Pipe-words remain a hard wall:
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
There’s a deeper post coming in future - probably named “The Geometry of a Language” - which will go deeper into the reasoning behind these new evaluation mechanisms, how it compares to other languages, and why this model could make code more visually exact, local and flexible.
Simpler Type Constructors
Value constructors were always just the name of the type in Rye. context, dict, list, failure, vector. But there were also to-<type>, like to-integer, to-string words that convert to that type. But the difference between constructing a type and converting to a type is not that clear.
Constructors also converted to a type from their inputs. We are joining this and removing all “to-” to make this more consistent and less noisy, hopefully.
| 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
Context Constructor Accepts Dict
Because to-context was merged with context function, it now takes both a block (evaluated) and a dict (converted directly):
; 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 Method Naming
File-URI methods dropped the redundant File- prefix:
| 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"
Flexible Failure Constructor
failure now accepts its arguments in any order - type, status code, and message are identified by their type, not position:
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" }
Affects all failure construction:
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
Migration Notes
If you’re upgrading a Rye script from 0.1.x:
- Replace
to-*constructors:to-string->string,to-integer->integer, etc. - Math with op-words now evaluates strictly left-to-right. If you had parentheses workarounds for basic arithmetic, you can probably simplify them.
- File-URI method names:
File-ext?->Ext?,Filename?->Name?. - Dotword chaining works naturally now -
"hello" .upper .trim .split " "evaluates left-to-right as expected.
As always, feedback is welcome on GitHub or try it out at ryelang.org. For posts directly from the grinder, visit r/ryelang.