Reaxis: A Natural Model for Reactive Programming in Haro, by Softanza
Reactive programming suffers from a profound semantic crisis. We've borrowed metaphors from plumbing ("backpressure"), event management ("handlers"), telephone systems ("callbacks"), and publishing ("subscribers/producers"). Each metaphor brings its own mental baggage, creating a fragmented conceptual landscape that obscures rather than illuminates the elegant simplicity underneath.
by Mansour Ayouni · in the repository since 2025-08-31 · paradigm essay · area Concurrency & distribution
The Language Crisis in Reactive Programming
Consider the cognitive load imposed by traditional reactive terminology: a programmer must mentally juggle "observables," "subscribers," "backpressure," "cold vs hot streams," "schedulers," and "operators"—each term importing complex metaphors from different domains. This semantic chaos transforms what should be natural data flow into an exercise in metaphor management.
The Softanza Reaxis system emerged from a fundamental question: what if we designed reactive semantics from the ground up, using natural language that describes what actually happens rather than what it reminds us of?
The Semantic Engineering Journey
Our semantic engineering effort began with this deceptively simple piece of code:
/*--- The code that sparked a semantic revolution ---*/
Rs = new stzReactiveSystem()
Rs {
oPriceStream = CreateStreamXT("my-price-api", OPTIMISED_FOR_NETWORK_SOURCE)
oPriceStream {
Transform(func price { return price * 1.20 })
Filter(func price { return price >= 120 })
OnPassed(func item { SaveToDatabase(item) NotifyUsersAbout(item) })
OnNoMore(func() { CloseConnections() SendSummary() })
OnError(func error { Log(error) AlertTeamAbout(error) })
anTestPrices = [ 80, 90, 95, 100, 120, 150, 200 ]
ReceiveMany(anTestPrices)
}
RunLoop()
}illustration
This code reveals the first breakthrough: reactive programming naturally follows a declare-then-execute pattern. We describe what should happen (Transform, Filter, OnPassed), then bring it to life (RunLoop()). Yet traditional reactive libraries obscure this natural flow with complex metaphors.
Challenge 1: The Callback Confusion
The term "callback" imports a telephone metaphor—someone calls you back later. "Handler" suggests event management—you handle situations as they arise. Both create unnecessary cognitive overhead for what is simply: functions that wait.
The Reaxis Solution: Rfunctions
We coined "Rfunction" to capture the essential distinction: these are functions in a declarative waiting state, activated only when the reactive system flows data through them. Unlike regular functions that execute immediately when called, Rfunctions exist in suspended animation until the stream brings them to life.
# Not a "callback" or "handler" - it's an Rfunction in waiting
Transform(func price { return price * 1.20 }) # Waits for stream activationillustration
Rfunctions also support localized error management through OnSuccess() and OnError() definitions within each function, eliminating the complex error propagation chains that plague traditional reactive systems.
Challenge 2: The Producer/Consumer Theater
Traditional reactive programming forces you to think about "who produces what for whom"—a complex choreography of roles and responsibilities. This external relationship management distracts from the core task: data transformation.
The Reaxis Solution: Stream-Centric Flow
We eliminated producer/consumer terminology entirely. Instead:
Receive()clearly indicates data entering the streamreturnwithin Rfunctions shows data flowing to the next stage- The stream itself manages all coordination
# No producer/consumer complexity - just natural data flow
oPriceStream {
Transform(func price { return price * 1.20 }) # Data flows in...
Filter(func price { return price >= 120 }) # ...transforms...
OnPassed(func item { SaveToDatabase(item) }) # ...and flows out
}illustration
Challenge 3: The Backpressure Metaphor
"Backpressure" suggests pressure pushing backward—a hydraulic metaphor that confuses more than clarifies. What's actually happening? Data arrives faster than it can be processed.
The Reaxis Solution: Overflow Strategy
We use "overflow"—a natural, immediately comprehensible term. When a container overflows, you understand exactly what happened and what your options are.
# Clear, natural overflow management
SetOverflowStrategy(BUFFER_EXPAND)
OnOverflow(func() { ? "Processing backlog - consider scaling" })illustration
The Conceptual Architecture: Container → Stream → Rfunction
Traditional reactive documentation never shows you the complete mental model. Here's what Reaxis makes explicit: [ 100, 80, 90, ]
╭─────────────────────── REACTIVE SYSTEM CONTAINER ─────────────────────────╮
│ │
│ ┌─ SHARED INFRASTRUCTURE ────────────────────────────────────────────┐ │
│ │ TimerManager • HTTPClient • TaskScheduler • StreamFactory │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ STREAM: "price-processor" ───────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌─ RECEIVE ─┐ ┌─ BUFFER ───────┐ ┌─ QUEUE ─┐ ┌─ RFUNCTION CHAIN ─┐ │ │
│ │ │ │ │ Cap: 1000 items │ │ 1. 100 │ │ Transform() │ │ │
Data ----│--> [100,80, │-->│ Use: 847 │-->│ 2. 80 │-->│ ↓ (*1.20) │---> DB
│ │ │ 90,95, │ │ │ │ 3. 90 │ │ Filter() │ │ │
│ │ │ 120,75] │ │ Overflow: │ │ 4. 95 │ │ ↓ (≥120) │ │ │
│ │ │ │ │ •EXPAND │ │ 5. 120 │ │ OnPassed() │ │ │
│ │ │ │ │ •REJECT │ │ 6. 75 │ │ ↓ (Save&Notify) │ │ │
│ │ │ │ │ •EVICT │ │ ↑ │ │ │ │ │
│ │ │ │ │ •BLOCK │ │ │ │ │ OnNoMore() │ │ │
│ │ └──────────┘ │ │ │ │ │ │ OnError() │ │ │
│ │ │ OnOverflow() │ │ │ │ │ (localized) │ │ │
│ │ └───────────────┘ └───┼────┘ └─────────────────┘ │ │
│ └───────────────────────────────────────┼───────────────────────────┘ │
│ │ │
│ ┌─ LIBUV EVENT LOOP ───────────────────────────────────────────────────┐ │
│ │ Network I/O • Timers • File System • Process Management │ │
│ └────────────────────────────────────────────────────────────────────┘ │
╰──────────────────────────────────────────────────────────────────────────╯
Key Architectural Insights:
- Container Level: ReactiveSystem provides shared services—all streams share timers, networking, task scheduling
- Stream Level: Independent data pipelines with four distinct stages
- Rfunction Level: Sequential processing functions that await activation
- LibUV Foundation: Asynchronous I/O infrastructure drives the entire system
Data Flow Precision: - RECEIVE: Pure intake, no processing - BUFFER: Traffic spike absorption with overflow strategies - QUEUE: Small (5-10 items), maintains processing order - RFUNCTIONS: Sequential pipeline where each stage can transform, filter, or terminate flow
This three-tier hierarchy (Container → Stream → Rfunction) eliminates the conceptual confusion of traditional reactive systems by making system boundaries and data flow paths explicitly visible.
Following the Data Journey Through Reaxis Architecture
Let's trace how our price data travels through the complete Reaxis pipeline. When we execute ReceiveMany([80, 90, 95, 100, 120, 150, 200]), each price begins a four-stage journey: Receive → Buffer → Queue → Rfunctions.
Stage 1: Reception
The number 100 enters through the single entry point—the Receive section. This is purely intake, with no processing logic.
Stage 2: Buffer Management
The item moves to the Buffer, which handles capacity and traffic spikes. If the buffer is at capacity (say 847/1000 items), overflow strategies activate:
BUFFER_EXPAND: Store excess items beyond original capacityBUFFER_REJECT_NEWEST: Reject the incoming 100BUFFER_EVICT_OLDEST: Replace oldest with incoming 100BUFFER_BLOCK: Block reception until capacity becomes available
In this case, space exists, so 100 is stored.
Stage 3: Queue Processing
The Queue takes items from the buffer in order (FIFO by default) and feeds them one-by-one to the processing pipeline. The queue is small (5-10 items) and focused solely on maintaining processing sequence.
Stage 4: Rfunction Pipeline
The first Rfunction, Transform(), awakens and multiplies 100 by 1.20, producing 120. It passes this to Filter(), which checks if 120 ≥ 120. It passes, flowing to OnPassed() for database storage and user notification.
Meanwhile, 80 follows the same path: transforms to 96, fails the filter (96 < 120), and gets dropped silently.
This separation eliminates mental gymnastics. No "subscribers" to coordinate, no "backpressure" hydraulics—just natural data flow through distinct, purpose-built stages.
The Buffer: Managing Traffic Reality
Traditional reactive programming conflates queue overflow with backpressure, creating conceptual confusion. Reaxis separates these concerns cleanly.
Buffer Purpose
The Buffer exists because data rarely arrives at the perfect processing rate. Network bursts, sensor spikes, and batch uploads create traffic mismatches that the buffer absorbs naturally.
oSensorStream = CreateStream("temperature-readings")
oSensorStream {
# Buffer configuration - handles traffic spikes
SetBufferSize(2000) # in number of items
SetOverflowStrategy(BUFFER_EXPAND)
# Queue configuration - handles processing flow
SetQueueSize(10)
SetQueueOrder(:FIFO)
Transform(func reading { SlowAnalysis(reading) }) # Intentionally slow
OnPassed(func result { SaveToDatabase(result) })
}illustration
Buffer Strategies
When the buffer overflows, clear strategies activate:
- BUFFER_EXPAND - Store excess items beyond the original capacity
- BUFFER_REJECT_NEWEST - Reject new items, preserve what's already buffered
- BUFFER_EVICT_OLDEST - Keep newest data, discard oldest
- BUFFER_BLOCK - Block reception until capacity becomes available
Buffer Monitoring
OnBufferOverflow(func(oStats) {
? "Buffer overflow: " + oStats.CurrentSize + "/" + oStats.Capacity
? "Items dropped: " + oStats.DroppedCount + " (strategy: " + oStats.Strategy + ")"
# Natural response - scale processing or adjust buffer
if oStats.OverflowFrequency > 0.1
ScaleProcessingPower()
ok
})illustration
The Container Principle: Infrastructure as Foundation
Streams cannot exist in isolation—they need infrastructure for timers, HTTP clients, and concurrency management. The ReactiveSystem container provides these shared services.
Rs = new stzReactiveSystem() # Container provides infrastructure
Rs {
# Multiple streams sharing resources
oPriceStream = CreateStreamXT("prices", :NETWORK_OPTIMIZED)
oLogStream = CreateStreamXT("logs", :FILE_OPTIMIZED)
oAlertStream = CreateStreamXT("alerts", :TIMER_OPTIMIZED)
# Single activation command for entire ecosystem
RunLoop()
}illustration
This eliminates resource management complexity that plagues traditional reactive systems where stream coordination becomes architectural chaos.
The Declare-Then-Execute Pattern
Reaxis mirrors natural human planning: describe desired behavior, then execute it.
Rs = new stzReactiveSystem()
Rs {
# PHASE 1: DECLARE - Describe behavior without execution
oMonitorStream = CreateStreamXT("temperature-monitor", :SENSOR)
oMonitorStream {
# Buffer for sensor bursts
SetBufferSize(500)
SetOverflowStrategy("latest") # Accept newest, drop oldest
# Processing pipeline
Transform(func temp {
return [temp, CurrentTime(), CalculateHeatIndex(temp)]
})
Filter(func data { return data[3] > DANGER_THRESHOLD })
OnPassed(func alert {
TriggerEmergencyAlert(alert[1], alert[2])
LogCriticalEvent(alert)
})
OnBufferOverflow(func(oStats) {
? "Sensor data overflow - possible sensor malfunction"
SwitchToBackupSensor()
})
# Data source
Receive(ReadSensorBatch())
}
# PHASE 2: EXECUTE - Activate the complete system
RunLoop()
}illustration
This explicit separation eliminates the imperative/declarative confusion haunting traditional reactive programming. The execution timing is always clear.
Overflow vs Backpressure: Natural Terminology
Traditional systems burden programmers with "backpressure strategies"—hydraulic metaphors requiring understanding of pressure differentials. Reaxis uses "overflow"—intuitive to every programmer.
Natural Overflow Management
oHighVolumeStream = CreateStream("data-firehose")
oHighVolumeStream {
# Clear, descriptive overflow handling
SetBufferSize(1000)
SetOverflowStrategy("buffer") # or :BUFFER_NEW_ITEM
Transform(func item { ProcessSlowly(item); return item })
OnBufferOverflow(func(oStats) {
? "Buffer overflow: " + oStats.CurrentSize + "/" + oStats.Capacity
AutoScaleProcessors() # Natural response
})
OnPassed(func result { DeliverResult(result) })
}illustration
Buffer Control Methods
When items accumulate, precise control is available:
ProcessNextFromBuffer()- Process one buffered itemProcessAllBuffered()- Process entire buffer contentsDrainBuffer(nCount)- Process specific number of itemsClearBuffer()- Empty buffer (emergency situations)
The mental model is visual and intuitive: when containers overflow, excess items spill according to clear strategies. No hydraulic engineering degree required.
Stream Optimization: Performance Without Complexity
Traditional reactive systems often hide performance considerations behind complex abstractions. Reaxis makes optimization explicit and optional through the stream type system.
# Different optimizations for different data sources
apiStream = CreateStreamXT("api-data", OPTIMISED_FOR_NETWORK_SOURCE)
fileStream = CreateStreamXT("log-processor", OPTIMISED_FOR_FILE_SOURCE)
sensorStream = CreateStreamXT("iot-data", OPTIMISED_FOR_SENSOR_SOURCE)
timerStream = CreateStreamXT("heartbeat", OPTIMISED_FOR_TIMER_SOURCE)
# But all streams use identical processing code
apiStream {
Transform(func data { return ProcessData(data) })
OnPassed(func result { SaveResult(result) })
}
fileStream {
Transform(func data { return ProcessData(data) }) # Same code!
OnPassed(func result { SaveResult(result) }) # Universal pattern!
}illustration
The optimization hints guide the LibUV infrastructure for efficient resource allocation, but the programmer writes universal stream processing code. This separation of concerns eliminates the false choice between performance and code simplicity.
Cross-Stream Communication: Natural Data Choreography
Real applications frequently require multiple streams processing disparate data sources. Traditional reactive systems introduce complexity through subscription management and stream merging operators. Reaxis treats multi-stream coordination as natural, independent data flows that communicate explicitly.
Each stream in Reaxis maintains complete processing autonomy—its own internal queue, its own Rfunctions pipeline, its own error handling. This isolation prevents the cascading failures and tight coupling that plague traditional reactive architectures. Stream-to-stream communication occurs through explicit Feed() and FeedMany() operations. Unlike implicit subscriptions or complex merging operators, these functions make data flow paths immediately transparent:
Rs = new stzReactiveSystem()
Rs {
# Input validation stream - processes raw data
oInputValidator = CreateStream("input-validator")
oInputValidator {
Transform(func rawData {
return CleanAndValidate(rawData)
})
Filter(func data {
return data.isValid
})
OnPassed(func validData {
# Explicit feeding to next stream
oBusinessProcessor.Feed(validData) # Single item feed
})
OnError(func invalidData {
? "Validation failed: " + invalidData.error
# Error isolation - this stream's error doesn't affect others
})
}
# Business logic stream - completely isolated pipeline
oBusinessProcessor = CreateStream("business-processor")
oBusinessProcessor {
Transform(func data {
return ApplyBusinessRules(data)
})
Filter(func processedData {
return processedData.passesCompliance
})
OnPassed(func compliantData {
# Feed to multiple downstream streams
oOutputFormatter.Feed(compliantData)
oAuditLogger.Feed(compliantData) # Parallel feeding
})
OnError(func error {
? "Business processing error: " + error
oErrorHandler.Feed(error) # Dedicated error stream
})
}
# Output formatting stream - independent processing
oOutputFormatter = CreateStream("output-formatter")
oOutputFormatter {
Transform(func data {
return FormatForAPI(data)
})
OnPassed(func finalOutput {
DeliverToClient(finalOutput)
})
}
# Audit logging stream - parallel processing
oAuditLogger = CreateStream("audit-logger")
oAuditLogger {
Transform(func data {
return CreateAuditRecord(data)
})
OnPassed(func auditRecord {
SaveToAuditDatabase(auditRecord)
})
}
# Error handling stream - processes all errors
oErrorHandler = CreateStream("error-processor")
oErrorHandler {
Transform(func error {
return AnalyzeError(error)
})
OnPassed(func analysis {
NotifyOpsTeam(analysis)
UpdateMetrics(analysis)
})
}
# Initial data entry point
rawDataBatch = GetIncomingData()
oInputValidator.FeedMany(rawDataBatch) # Batch feeding
RunLoop()
}
This explicit communication model creates maintainable, debuggable reactive systems where data flow is always visible and streams remain truly independent.
The Mental Model Transformation
The visual architecture diagram transforms how programmers think about reactive systems. Instead of abstract "observables" and "observers," you see concrete components with clear responsibilities:
The Container Level: ReactiveSystem provides infrastructure and coordinates the event loop. This is where system-wide services live—HTTP clients, timers, file handlers.
The Stream Level: Each stream has three distinct regions—Receive (data entry), Internal Queue (buffering), and Rfunctions (processing logic). Data flows left to right through these regions.
The Function Level: Rfunctions wait in their designated region until stream data activates them. They process one item, pass results forward, and return to waiting state.
This three-level mental model eliminates the conceptual confusion that plagues traditional reactive programming. You always know where you are (container, stream, or function level) and what your responsibilities are at each level.
Practical Mastery Through Natural Patterns
Let's explore how this mental model handles real-world complexity. Consider a sensor monitoring system that must process temperature readings, detect anomalies, and trigger alerts:
Rs = new stzReactiveSystem()
Rs {
oTemperatureMonitor = CreateStreamXT("temp-sensor", OPTIMISED_FOR_SENSOR_SOURCE)
oTemperatureMonitor {
# Each reading gets contextual data attached
Transform(func temperature {
return {
:value = temperature,
:timestamp = CurrentTime(),
:location = GetSensorLocation(),
:baseline = GetBaselineTemp()
}
})
# Filter for potential anomalies
Filter(func reading {
deviation = Abs(reading.value - reading.baseline)
return deviation > ANOMALY_THRESHOLD
})
# Handle confirmed anomalies
OnPassed(func anomaly {
severity = CalculateSeverity(anomaly)
if severity > CRITICAL_LEVEL {
TriggerImmediateAlert(anomaly)
NotifyEmergencyTeam(anomaly)
else
LogAnomalyEvent(anomaly)
ScheduleMaintenanceCheck(anomaly.location)
}
})
# Graceful completion when sensor goes offline
OnNoMore(func() {
? "Sensor offline - switching to backup monitoring"
ActivateBackupSensor()
})
# Error recovery for sensor malfunctions
OnError(func error {
? "Sensor error: " + error.message
AttemptSensorReset()
if ResetFailed() {
SwitchToManualMonitoring()
}
})
}
# Set up periodic sensor reading
Rs.RunEvery(5, :Seconds, func() { # Every 5 seconds
reading = ReadTemperatureSensor()
oTemperatureMonitor.Receive(reading)
})
RunLoop()
}illustration
The mental model makes this complex scenario tractable. You see data entering through Receive, flowing through the queue, activating Rfunctions in sequence. When anomalies are detected, the flow branches naturally to appropriate responses. When the sensor fails, error Rfunctions maintain system stability.
Advanced Patterns: Stream Orchestration
For complex applications requiring multiple coordinated streams, the mental model scales elegantly. Consider a real-time trading system that must correlate market data, news feeds, and risk assessments:
Rs = new stzReactiveSystem()
Rs {
# Three specialized streams with different optimization profiles
oMarketData = CreateStreamXT("market-feed", OPTIMISED_FOR_NETWORK_SOURCE)
oNewsStream = CreateStreamXT("news-analysis", OPTIMISED_FOR_NETWORK_SOURCE)
oRiskEngine = CreateStreamXT("risk-calculator")
# Market data processing
oMarketData {
Transform(func quote {
return EnrichWithTechnicalIndicators(quote)
})
Filter(func enrichedQuote {
return enrichedQuote.volume > MIN_VOLUME
})
OnPassed(func marketSignal {
# Feed both other streams
oNewsStream.Receive(marketSignal.symbol)
oRiskEngine.Receive(marketSignal)
})
# Feed market data within container
testQuotes = GetTestMarketData()
ReceiveMany(testQuotes)
}
# News impact analysis
oNewsStream {
Transform(func symbol {
news = GetRecentNews(symbol)
sentiment = AnalyzeSentiment(news)
return { :symbol = symbol, :sentiment = sentiment }
})
OnPassed(func newsAnalysis {
oRiskEngine.Receive(newsAnalysis) # Feed to risk calculation
})
}
# Risk assessment and decision
oRiskEngine {
SetOverflowStrategy(:KEEP_NEWEST, 10) # Fast-moving data
Transform(func input {
# Correlate market and news data
if input.type = "market" {
StoreMarketData(input)
but input.type = "news"
StoreNewsData(input)
}
return CalculateRiskScore()
})
Filter(func riskScore {
return riskScore.confidence > MIN_CONFIDENCE
})
OnPassed(func decision {
if decision.action = "BUY" {
ExecuteTrade(decision)
but decision.action = "SELL"
ExecuteSell(decision)
but decision.action = "HOLD"
UpdatePortfolioStatus(decision)
}
})
OnError(func error {
HaltTrading()
NotifyRiskManagers(error)
})
}
RunLoop()
}illustration
Even this complex orchestration remains mentally tractable because each stream follows the same visual pattern: data enters, flows through the queue, activates Rfunctions in sequence. The communication between streams happens through explicit Receive() calls, making data flow paths immediately visible.
The Performance Architecture
The optimization constants (OPTIMISED_FOR_NETWORK_SOURCE, etc.) deserve special attention because they solve a critical problem in reactive programming: performance optimization without code complexity.
Traditional reactive systems force you to choose between simple code and optimal performance. Reaxis eliminates this false choice by separating optimization hints from processing logic:
# Same processing code, different optimizations
oApiStream = CreateStreamXT("api", OPTIMISED_FOR_NETWORK_SOURCE)
# ↑ LibUV optimizes for network I/O patterns
oFileStream = CreateStreamXT("logs", OPTIMISED_FOR_FILE_SOURCE)
# ↑ LibUV optimizes for file system patterns
oTimerStream = CreateStreamXT("scheduler", OPTIMISED_FOR_TIMER_SOURCE)
# ↑ LibUV optimizes for high-precision timing
# But all use identical stream processing patterns
allStreams.forEach(func stream {
stream {
Transform(func data { return ProcessData(data) })
OnPassed(func result { DeliverResult(result) })
}
})illustration
This approach gives you enterprise-grade performance with beginner-friendly code. The optimization happens transparently in the LibUV infrastructure while your stream processing code remains universal and maintainable.
Localized Error Management: Eliminating Error Propagation Complexity
Traditional reactive systems suffer from error propagation nightmares—exceptions bubble up through complex chains, requiring global error handling strategies and subscription management. Reaxis eliminates this entirely through localized error management.
Every Rfunction can define its own error recovery:
oPaymentProcessor = CreateStreamXT("payments", OPTIMISED_FOR_NETWORK_SOURCE)
oPaymentProcessor {
Transform(func payment {
OnSuccess(func result {
return EnrichPaymentData(result)
})
OnError(func error {
? "Transform failed: " + error
return CreateErrorPayment(payment, error) # Local recovery
})
# Main processing logic
if not ValidatePayment(payment) {
raise "Invalid payment format"
}
return ProcessPayment(payment)
})
Filter(func payment {
OnSuccess(func passed {
LogFilterSuccess(passed)
return passed
})
OnError(func error {
? "Filter error: " + error
return false # Local decision: drop on error
})
return payment.amount > MIN_AMOUNT
})
OnPassed(func payment {
UpdateAccountBalance(payment)
SendConfirmation(payment)
})
# Stream-level error handling for unhandled errors
OnError(func error {
EscalateToSupport(error)
PauseStream()
})
}illustration
This localized approach means errors are handled exactly where they occur, with full context available. No complex exception chains, no global error strategies, no subscription cleanup—just local decisions about error recovery at each processing stage.
Time-Based Reactive Patterns
Timing patterns—delays, throttling, windowing—often require complex operator combinations in traditional systems. Reaxis handles time naturally through the ReactiveSystem infrastructure:
Rs = new stzReactiveSystem()
Rs {
oEmailProcessor = CreateStreamXT("email-queue")
oEmailProcessor {
Transform(func email {
return PrepareEmail(email)
})
OnPassed(func preparedEmail {
SendEmail(preparedEmail)
LogEmailSent(preparedEmail.recipient)
})
OnError(func error {
ScheduleRetry(error.originalEmail)
})
# Feed emails within stream container
if HasPendingEmails() {
emails = GetAllPendingEmails()
ReceiveMany(emails)
}
}
# Natural timing control through ReactiveSystem services
Rs.RunEvery(2.Seconds, func() {
if HasPendingEmails() {
nextEmail = GetNextPendingEmail()
oEmailProcessor.Receive(nextEmail)
}
})
RunLoop()
}illustration
Rate limiting happens naturally through the timing of Receive() calls. No complex operators, no timing abstractions—just straightforward scheduling through system services.
Natural Timing Operations: Beyond SetInterval and Debounce Confusion
Traditional timing functions suffer from the same metaphorical confusion that plagues reactive programming. SetInterval and SetTimeout force programmers to decode ambiguous terminology when the operations are conceptually simple.
Consider the cognitive load of this traditional approach:
# What does this actually do? When does it start? How often does it repeat?
Rs.SetInterval(5000, func() {
reading = ReadTemperatureSensor()
oTemperatureMonitor.Receive(reading)
})
# Is this a one-time delay? A maximum wait time? Initial startup delay?
Rs.SetTimeout(3000, func() {
InitializeSystem()
})illustration
The Reaxis Solution: Natural Timing Language
Reaxis eliminates timing ambiguity with immediately comprehensible functions:
# Crystal clear: wait 5 seconds, then execute, then wait 5 seconds, repeat
Rs.RunEvery(5, :Seconds, func() {
reading = ReadTemperatureSensor()
oTemperatureMonitor.Receive(reading)
})
# Unambiguous: wait 3000 milliseconds, then execute once
Rs.RunAfter(3000, :Milliseconds, func() {
InitializeSystem()
})illustration
Natural Timer Control: StopTimer() vs ClearInterval()
Traditional reactive frameworks inherit browser terminology that creates semantic inconsistency:
# Browser-inherited confusion intervalId = Rs.SetInterval(1000, callback) Rs.ClearInterval(intervalId) # "Clear" suggests erasing content, not stopping execution
illustration
Reaxis uses natural action verbs that match the timing operations:
# Natural, consistent semantics timerId = Rs.RunEvery(1, :Seconds, callback) Rs.StopTimer(timerId) # Clear action: "stop the timer" # Batch operations for system management Rs.StopAllTimers() # Emergency shutdown of all timing operations
illustration
Integrated Stream Processing with Natural Timing
This natural timing language integrates seamlessly with stream processing:
Rs = new stzReactiveSystem()
Rs {
oSensorMonitor = CreateStreamXT("sensors", OPTIMISED_FOR_SENSOR_SOURCE)
oSensorMonitor {
Transform(func reading {
return EnrichWithMetadata(reading)
})
Filter(func data {
return data.value > ALERT_THRESHOLD
})
OnPassed(func alert {
TriggerAlert(alert)
LogEvent(alert)
})
}
# Natural timing eliminates mental translation
sensorTimer = Rs.RunEvery(10, :Seconds, func() {
currentReading = ReadSensor()
oSensorMonitor.Receive(currentReading)
})
# Conditional timer control with natural semantics
Rs.RunAfter(60, :Seconds, func() {
if SystemOverloaded()
Rs.StopTimer(sensorTimer) # Natural: stop the specific timer
? "Sensor monitoring paused due to system load"
ok
})
# Delayed initialization with clear semantics
Rs.RunAfter(30, :Seconds, func() {
CalibrateAllSensors()
? "Sensor calibration complete"
})
RunLoop()
}illustration
Natural State Settling with WaitForAttributeToSettle()
Traditional reactive programming uses "debounce"—borrowed from electronics switch bounce elimination—creating cognitive overhead when the operation is conceptually simple. Developers must decode timing mechanics instead of focusing on business logic.
Consider the mental gymnastics required with traditional debouncing:
// What is "bouncing"? Why 800ms? When does it actually fire?
searchInput.pipe(
debounceTime(800),
distinctUntilChanged()
).subscribe(query => {
performSearch(query);
});
Reaxis eliminates debounce confusion with immediately comprehensible semantics:
# Crystal clear: wait for the attribute to stop changing, then act
oXSearch.WaitForAttributeToSettle(:@Query, 800, func(attr, oldval, newval) {
PerformSearch(newval) # Execute only when user stops typing
})illustration
Semantic Consistency Table
| Operation | Traditional | Reaxis Natural | Semantic Benefit |
|---|---|---|---|
| Start repeating | SetInterval() |
RunEvery() |
Action verb describes behavior |
| Start delayed | SetTimeout() |
RunAfter() |
Clear temporal relationship |
| Stop specific | ClearInterval() |
StopTimer() |
Direct action, no ambiguity |
| Stop all | Multiple clears | StopAllTimers() |
Batch operation clarity |
| Wait mechanism | debounceTime() |
WaitForAttributeToSettle() |
Describes actual behavior |
| State change | "bounce elimination" | "attribute settling" | Natural data flow language |
The timing and settling functions follow the same semantic engineering principles as the reactive system: use natural language that describes what actually happens rather than importing metaphors from other domains.
The Cognitive Load Revolution
The true breakthrough of the Reaxis mental model lies in cognitive load reduction. Traditional reactive programming requires juggling multiple complex metaphors simultaneously. Reaxis provides a single, coherent mental framework:
Container → Stream → Rfunction
Every reactive concept maps naturally to this hierarchy:
- System coordination happens at Container level
- Data flow happens at Stream level
- Processing logic happens at Rfunction level
This eliminates the mental context switching that exhausts programmers working with traditional reactive systems. You maintain a consistent perspective regardless of complexity.
Implementation Transparency
Unlike traditional reactive frameworks that hide complexity behind abstractions, Reaxis makes the implementation visible and understandable. The visual model shows exactly what's happening:
- Data enters through a single point (
Receive) - Gets queued for processing (Internal Queue)
- Activates Rfunctions in sequence (Transform → Filter → OnPassed)
- Handles overflow and errors naturally (OnOverflow, OnError)
- Concludes gracefully when complete (OnNoMore, Conclude)
This transparency enables confident debugging and performance optimization. When something goes wrong, you can visualize exactly where in the pipeline the problem occurs.
Reaxis vs. Existing Reactive Frameworks
To understand how radically Reaxis diverges from established approaches, examining its semantic choices against major frameworks reveals the scope of the innovation:
| Aspect | Reaxis | RxJS | Akka Streams | Project Reactor | Node.js Streams |
|---|---|---|---|---|---|
| Mental Model | Container→Stream→Rfunction | Observable/Observer pattern | Graph DSL | Publisher/Subscriber | Unix pipes |
| Key Metaphors | Natural overflow, Waiting functions | Hot/Cold streams, Marble diagrams, Backpressure | Hydraulic flow, Sources/Sinks | Reactive streams spec | Readable/Writable/Transform |
| Error Handling | Local per-function and per-stream | Exception chains, Retry operators | Supervision hierarchies | Error signals | Event-based callbacks |
| Performance Control | Explicit optimization hints | Hidden in operators | Material values | Schedulers | Manual buffering |
| Learning Curve | Gentle - descriptive language | Steep - complex metaphors | Very steep - functional concepts | Steep - many abstractions | Moderate - familiar metaphor |
| Core Concepts | OnPassed(), OnNoMore(), WaitFor(), Overflow() | subscribe(), onNext(), onError(), onComplete() | Source.via(), Sink.foreach(), Flow.map() | subscribe(), onNext(), onError(), onComplete() | pipe(), on('data'), on('end'), on('error') |
The Haro language implementation provides Reaxis with a crucial strategic advantage: complete semantic control without fighting established metaphors. JavaScript developers expect "observables," Java developers expect "publishers," and Scala developers expect "actors"—decades of accumulated reactive terminology that would resist the natural language approach.
Haro's relative obscurity becomes a feature rather than a limitation, allowing Reaxis to develop optimal semantics from first principles without competing against entrenched (but flawed) terminology. This semantic laboratory enables radical innovation that would be impossible in mainstream languages carrying heavy metaphorical baggage.
Conclusion: The Future of Reactive Programming
The Reaxis mental model represents more than a new reactive programming framework—it's a proof of concept for semantic engineering in programming language design. By questioning every inherited metaphor and replacing confusing terminology with natural, descriptive language, we've created a reactive system that programmers can understand intuitively.
The visual mental model, combined with the declare-then-execute pattern and natural terminology, eliminates the learning curve that has kept reactive programming inaccessible to many developers. More importantly, it reduces the cognitive load for experienced reactive programmers, allowing them to focus on business logic rather than metaphor management.
This is reactive programming as it should be: natural, visual, and immediately comprehensible. The industry has spent decades making reactive programming increasingly complex. Reaxis proves it can be elegantly simple instead.
The complete Softanza Reaxis system is available as part of the Softanza library for the Haro programming language, built on RingLibUV for high-performance asynchronous operations.
Cite
Mansour Ayouni. "Reaxis: A Natural Model for Reactive Programming in Haro, by Softanza". Softanza narrations, 2025. https://mayouni.github.io/stzsite/en/narrations/stz-reaxis-reactive-programming-narration.html
Not run: its code touches files, the network, the clock or chance. The article is published as written; its blocks are illustrations. The code of the articles is Haro code. Where an article was written before the language took its present name, the name was changed and nothing else. Genre, area and series read from the file's name, the year from its first commit: derived, until the article carries its own header. The file on GitHub(opens in a new tab)

