Softanza
DémarrerEnglish
Softanza
Recherche · narration

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.

par Mansour Ayouni · dans le dépôt depuis le 2025-08-31 · essai de paradigme · domaine Concurrence et distribution

Cet article est écrit en anglais, comme la bibliothèque.

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:

Haro
/*--- 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.

Haro
# Not a "callback" or "handler" - it's an Rfunction in waiting
Transform(func price { return price * 1.20 })  # Waits for stream activation

illustration

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 stream
  • return within Rfunctions shows data flowing to the next stage
  • The stream itself manages all coordination
Haro
# 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.

Haro
# 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:

  1. Container Level: ReactiveSystem provides shared services—all streams share timers, networking, task scheduling
  2. Stream Level: Independent data pipelines with four distinct stages
  3. Rfunction Level: Sequential processing functions that await activation
  4. 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 capacity
  • BUFFER_REJECT_NEWEST: Reject the incoming 100
  • BUFFER_EVICT_OLDEST: Replace oldest with incoming 100
  • BUFFER_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.

Haro
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

Haro
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.

Haro
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.

Haro
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

Haro
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 item
  • ProcessAllBuffered() - Process entire buffer contents
  • DrainBuffer(nCount) - Process specific number of items
  • ClearBuffer() - 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.

Haro
# 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:

Haro
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:

Haro
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:

Haro
# 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:

Haro
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:

Haro
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:

Haro
# 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:

Haro
# 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:

Haro
# 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:

Haro
# 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:

Haro
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:

Javascript, montré pour comparaison, non exécuté
// 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:

Haro
# 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.

Citer

Mansour Ayouni. « Reaxis: A Natural Model for Reactive Programming in Haro, by Softanza ». Softanza narrations, 2025. https://mayouni.github.io/stzsite/fr/narrations/stz-reaxis-reactive-programming-narration.html

Non exécuté : son code touche aux fichiers, au réseau, à l'horloge ou au hasard. L'article est publié tel qu'il est écrit ; ses blocs sont des illustrations. Le code des articles est du code Haro. Là où un article a été écrit avant que la langue prenne son nom actuel, le nom a été changé et rien d'autre. Genre, domaine et série lus dans le nom du fichier, année tirée du premier commit : dérivés, jusqu'à ce que l'article porte son propre en-tête. Le fichier sur GitHub(s'ouvre dans un nouvel onglet)