In Python, when you write if x > 5: print("big")
, you’re using special syntax baked into the language. You can’t change how if
works. You can’t compose it, pipe it, or partially apply it. You can’t pass if
as an argument to another function.
But what if you could? What if if
, for
, and while
were just regular functions that followed the same rules as everything else?
This isn’t just a theoretical curiosity—it fundamentally changes how you think about and write code.
Why This Matters
Consistency. Most languages have two sets of rules: one for functions and another for control structures. Languages like Rye eliminate this duplication—everything follows the same patterns.
Composability. Functions can be composed, piped, and partially applied. When control structures are functions, they inherit these powerful capabilities automatically.
Extensibility. You’re no longer limited to the control structures the language designer gave you. Need a new kind of loop? Just write a function.
Let’s see what this looks like in practice.
When Code Becomes Data
Here’s a conditional in Python:
temperature = 36
# We can evaluate and print an expression
print(temperature > 30)
# prints: True
# But we can't print a block of code without executing it
# (well, we could stringify it, but that's not the same)
# Standard conditional - special syntax
if temperature > 30:
print("It's hot!")
# prints: It's hot!
Now compare this to Rye:
temperature: 36
; Evaluates expression and prints the result
print temperature > 30
; prints: true
; In Rye, blocks are data - we can print them
print { print "It's hot!" }
; prints: print "It's hot!"
; The 'do' function evaluates a block
do { print "It's hot!" }
; prints: It's hot!
; And here's the conditional - also just a function
if temperature > 30 {
print "It's hot!"
}
; prints: It's hot!
Look at that last if
statement. It’s not special syntax—it’s a function call. The if
function takes two arguments:
- A condition that evaluates to true or false
- A block of code to run if the condition is true
You might wonder: “Won’t the block execute immediately when passed as an argument?” Here’s the key insight: in Rye, code blocks { ... }
are values, not computations. They don’t execute until you explicitly evaluate them. The if
function receives the block as data and decides whether to run it based on the condition.
This is the secret sauce: when code is data, control flow doesn’t need to be special.
One Pattern to Rule Them All
In Python, every language feature has its own syntax:
# Conditionals - keywords and colons
if x > 5:
y = "big"
# Loops - different syntax
for i in range(10):
print(i)
# Iteration - similar but distinct
for item in ["milk", "bread", "pickles"]:
print(item)
# Functions - def keyword, parentheses, colons, indentation
def add(a, b):
return a + b
In Rye, one pattern applies everywhere:
; Conditionals - function taking a boolean and a block
if x > 5 { y: "big" }
; Counting loop - function taking an integer and a block
loop 10 { .print }
; Iteration - function taking a collection and a block
for { "milk" "bread" "pickles" } { .print }
; Functions - function taking argument list and body block
add: fn { a b } { a + b }
Every construct follows the same shape: a name, followed by arguments, some of which happen to be blocks of code. Instead of memorizing Python’s 35+ keywords and their unique syntaxes, you learn one universal pattern.
The distinction between “built-in language features” and “library functions” disappears.
Composing Control Flow
In Python, if
and for
are statements, not values. But in Rye, they’re functions—and functions compose naturally.
loop either temperature > 32 { 3 } { 1 } { prns "Hot!" }
; Prints: Hot! Hot! Hot!
; Optional parentheses show evaluation order (like Lisp)
( loop ( either ( temperature > 32 ) { 3 } { 1 } ) { prns "Hot!" } )
; Prints: Hot! Hot! Hot!
(prns
prints a value followed by a space)
Here, either
(Rye’s ternary operator) is also just a function that returns one of two blocks based on a condition. We’re nesting function calls naturally—something impossible with Python’s statement-based control flow.
The Python equivalent requires multiple lines and variable mutation:
repeats = 1
if temperature > 32:
repeats = 3
for _ in range(repeats):
print("Hot!", end=' ')
# Prints: Hot! Hot! Hot!
Python does have ternary expressions as a special case:
for _ in range(3 if temperature > 32 else 1):
print("Hot!", end=' ')
# Prints: Hot! Hot! Hot!
But notice how Python needed to add yet another special syntax to make this work. In Rye, it’s just function composition.
Since control flow functions are ordinary values, you can store them and pass them around:
hot-code: { print "Hot!" }
is-hot: temperature > 30
if is-hot hot-code
; Prints: Hot!
loop 2 hot-code
; Prints: Hot!
; Hot!
Piping Into Control Flow
Rye supports piping data through functions using |
and .
operators. Since control structures are functions, they work with these operators too:
; Pipe a condition into if
temperature > 30 |if { print "Hot!" }
; Prints: Hot!
; Pipe a number into loop
3 .loop { .prns }
; Prints: 0 1 2
; Pipe a collection into for
{ "Hot" "Pockets" } |for { .print }
; Prints: Hot
; Pockets
In Python, you can’t pipe into if
or for
because they’re not values—they’re syntax.
Partial Application Creates New Control Structures
In Rye, you can partially apply functions to create specialized versions:
add-five: partial ?_+ [ _ 5 ]
add-five 10
; returns 15
three-times: partial ?loop [ 3 _ ]
; Use it like any other function
three-times { prns "Hey!" }
; Prints: Hey! Hey! Hey!
You’ve just created a custom control structure by partially applying a built-in one. Try doing that with Python’s for
loop.
Higher-Order Control Flow
Since control structures are functions, you can pass them to other functions and transform them:
; Create a debugging wrapper for any two-argument function
verbosify\2: fn { fnc } {
closure { a b } {
probe a
probe b
fnc a b
}
}
; Apply it to regular functions
myconcat: verbosify\2 ?concat
myconcat "AAA" "BBB"
; Prints:
; [String: AAA]
; [String: BBB]
; Returns: "AAABBB"
; Apply it to control flow functions too!
myif: verbosify\2 ?if
myif temperature < 30 { print "cold!" }
; Prints:
; [Boolean: false]
; [Block: ^[Word: print] [String: cold!] ]
The same higher-order function works on both regular functions and control flow—because there’s no difference.
You Are the Language Designer
Since control flow is just functions, you can write your own control structures indistinguishable from built-ins.
Custom Control Structures
Want the opposite of if
? Write unless
:
; Want the opposite of if?
unless: fn { condition block } {
if not condition block
}
unless tired {
print "Keep working!"
}
Need an until
loop?
until: fn { condition block } {
loop {
r:: do block
if do condition { return r }
}
}
count:: 0
until { count > 5 } {
print count
count:: inc count
}
; Prints: 0 1 2 3 4 5
Dynamic Control Flow Selection
You can even choose control structures at runtime:
; Store control functions in variables
control: either invert-logic { unless } { if }
; Use the selected control structure
control condition { do-something }
Library-Level Control Structures
The boundary between “language features” and “library functions” dissolves completely. Consider these examples that feel like built-in language constructs:
; Pattern matching (could be from a library)
switch password {
"sesame" { "Opening ..." }
"123456" { "Self destructing!" }
}
; Fizz-buzz using a cases function
for range 1 100 { :n
cases " " {
{ n .multiple-of 3 } { "Fizz" }
{ n .multiple-of 5 } { + "Buzz" }
_ { n }
} |prns
}
; Outputs: 1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16 ...
Even external libraries can provide domain-specific control structures. For example, an OpenAI library might have a streaming chat function that works like a loop:
openai Read %.token
|Chat\stream "A joke of the day?" { .prn }
; Streams the response character by character
The function accepts a block of code and calls it for each streaming chunk—just like for
loops over collections.
Real-World Benefits
Code as Configuration
When control structures are data, configuration becomes more powerful:
; Define different processing strategies
strategies: [
[ "development" { if debug-mode } ]
[ "production" { unless errors-suppressed } ]
[ "testing" { loop test-iterations } ]
]
; Select and use a strategy
environment |get strategies |apply* process-data
Macros Without Macros
You can create macro-like abstractions using ordinary functions:
; A "macro" for timing code execution
timed: fn { description block } {
start: now
result: do block
elapsed: now - start
print [description "took" elapsed "ms"]
result
}
; Use it like built-in syntax
timed "Database query" {
db-query "SELECT * FROM users"
}
The Trade-offs
Performance Considerations
Python can optimize special forms at compile time because it knows exactly what if
and for
do. In Rye, these are function calls with potential runtime overhead.
However, this isn’t necessarily a fundamental limitation. A smart compiler could:
- Inline common control flow functions
- Optimize when it detects standard usage patterns
- Apply the same optimizations to user-defined control structures
The key insight: you optimize one thing (function calls) instead of dozens of special syntactic forms.
Tooling Challenges and Opportunities
IDEs excel at understanding Python’s if
, for
, def
keywords because they’re baked into the language grammar. When everything is a function, traditional syntax highlighting and code completion lose some precision.
But this creates new opportunities:
- Simpler parsers: One syntax rule instead of dozens
- Extensible tooling: Tools can learn about new control structures dynamically
- Consistent behavior: The same refactoring and analysis tools work on all constructs
Beyond Control Flow
This principle extends beyond just if
and for
. What if everything in a language followed the same rules?
; Variable definition as a function
let { x: 5, y: 10 } { x + y }
; Module imports as functions
use { "math" "json" "http" } {
; Now math, json, http are available
}
; Even syntax could be redefinable
infix-op: fn { precedence symbol implementation } {
; Define new operators at runtime
}
When the language has one universal mechanism instead of dozens of special cases, both humans and tools benefit from the consistency.
Conclusion
Making control flow into functions isn’t just about syntax—it’s about eliminating artificial boundaries in programming languages.
Most languages draw arbitrary lines: “These are language features (special), these are library functions (ordinary).” But why should if
be special while map
is ordinary? Why can you compose map
with filter
but not if
with loop
?
Languages like Rye and REBOL suggest a different path: one rule for everything. The result is fewer concepts to learn, more ways to combine existing pieces, and the power to extend the language when the built-in pieces aren’t enough.
This isn’t just theoretical—it changes how you think about code. When you realize control flow is just functions, you start seeing composition opportunities everywhere. When you realize syntax is just data, you start building your own abstractions. When you realize there’s no fundamental difference between “language” and “library,” you become a language designer yourself.
The next time you’re frustrated by a language’s limitations, ask yourself: what if everything were just functions?
Want to explore these ideas further? Check out Rye and REBOL to see uniform function-based languages in action.