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:

  1. A condition that evaluates to true or false
  2. 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.