Back to blog

Railway-Oriented Programming avec Rust & Polarway

Railway-Oriented Programming with Rust & Polarway

📖 Abstract

Functional programming in Rust — through the Result<T, E> and Option<T> types — offers a radically different approach to error handling and pipeline composition. The Railway-Oriented Programming (ROP) pattern models execution as two parallel tracks: the happy track (success) and the error track. Polarway reifies this paradigm in Python via PyO3, enabling robust, composable data pipelines compiled to high-performance Rust – no exceptions, no runtime overhead.

📋 Table of Contents

  1. Why Functional Programming?
  2. The Railway-Oriented Pattern
  3. Result and Option Monads in Rust
  4. Zero-Cost Abstractions with PyO3
  5. Mathematical Foundations
  6. Polarway Pipelines in Practice
  7. References

1. Why Functional Programming?

High-frequency trading pipelines are intrinsically compositions of transformations on data streams: read → validate → clean → normalise → compute → send orders. Each step can fail. Classical imperative approaches (Python try/except, C error codes) produce code that is hard to compose, test, and reason about.

Functional programming addresses this by treating errors as first-class values, threaded explicitly through the chain rather than thrown sideways via exceptions. In Rust, this materialises as:

Approach Advantage Zero Cost?
Result<T, E>Compiler-checked errors
Option<T>Explicit nullability, no NPE
.map() / .and_then()Composable chaining without boilerplate
IteratorsLazy evaluation + SIMD auto-vectorisation
try/except PythonFamiliar❌ (stack unwind)
Core principle: In Rust, functional abstractions (map, and_then, filter_map…) are erased at compile time. They cost nothing at runtime while enforcing correctness.

2. The Railway-Oriented Pattern

2.1 The Two-Track Metaphor

Popularised by Scott Wlaschin, Railway-Oriented Programming visualises a pipeline as two tracks: the happy track (success) and the error track. Each function in the pipeline is a switch: it receives a value on the happy track, processes it, and either continues on the happy track or switches to the error track.

Railway Pattern Visualisation

Railway-Oriented Programming — Dual Track Pipeline
OK
read_csv()
validate()
clean()
compute()
✓ Result
⤵ error ⤵ error ⤵ error
ERR
IoError
SchemaError
ParseError
✗ Error
Once on the error track → all subsequent steps are bypassed automatically

In Rust, this pattern is directly implemented by the Result<T, E> type. The ? operator and the .map(), .and_then() methods are the switches that route the value onto one track or the other.

// Railway-Oriented Programming in Rust
// Each step is a "switch" — success stays on Ok track, error goes to Err track

fn process_data(path: &str) -> Result<DataFrame, PolarwayError> {
    read_csv(path)                         // Try to read
        .and_then(|df| validate_schema(df)) // If Ok, validate
        .and_then(|df| clean_missing(df))   // If Ok, clean
        .and_then(|df| apply_signals(df))   // If Ok, apply signals
        .map(|df| normalise(df))            // If Ok, normalise (infallible)
    // If ANY step returns Err, the chain short-circuits immediately
}
Crucial advantage: The Rust compiler enforces error handling. It is impossible to use a Result without checking whether it contains an error — no silent error can propagate.

2.2 Comparison: Pandas vs Polarway

Error Handling — Pandas vs Polarway (Railway)
❌ Pandas / exceptions
try:
df = read_csv("...")
val = df["col"][0]
except Exception as e:
print(e) # silent?
KeyError, IndexError, silent NaN propagation
✅ Polarway / Result
result = (
read_csv("...")
.and_then(validate)
.map(clean)
)
Type-safe · Composable · Zero overhead

3. Result and Option Monads in Rust

3.1 Result<T, E> — The Error Monad

Result<T, E> is a sum type (enum) with two variants: Ok(T) for success and Err(E) for failure. It implements the monad structure through the map and and_then methods.

// Core Result API
enum Result<T, E> {
    Ok(T),
    Err(E),
}

impl<T, E> Result<T, E> {
    // Functor: map over Ok value (infallible transform)
    fn map<U, F: Fn(T) -> U>(self, f: F) -> Result<U, E>

    // Monad: flatMap / bind (fallible transform)
    fn and_then<U, F: Fn(T) -> Result<U, E>>(self, f: F) -> Result<U, E>

    // Map the error variant
    fn map_err<F, O: Fn(E) -> F>(self, op: O) -> Result<T, F>
}

// The ? operator = automatic error propagation
fn step1(x: f64) -> Result<f64, MyError> { Ok(x * 2.0) }
fn step2(x: f64) -> Result<f64, MyError> { if x > 0.0 { Ok(x) } else { Err(MyError::NegativeValue) } }

fn pipeline(x: f64) -> Result<f64, MyError> {
    let a = step1(x)?;  // ? = propagate Err automatically
    let b = step2(a)?;
    Ok(b + 1.0)
}

3.2 Option<T> — The Nullability Monad

Option<T> replaces Python's null / None in a compiler-verified way. An Option<T> is either Some(T) (value present) or None (absent). The compiler forbids using the value without checking which variant is present.

// No more NullPointerException / AttributeError: 'NoneType'
fn best_bid(orderbook: &Orderbook) -> Option<f64> {
    orderbook.bids.first().map(|bid| bid.price)
    // Returns None if no bids — no panic, no undefined behavior
}

// Chain optional computations safely
fn spread(ob: &Orderbook) -> Option<f64> {
    let bid = best_bid(ob)?;      // None if no bid
    let ask = best_ask(ob)?;      // None if no ask
    Some(ask - bid)               // Only reached if both exist
}

4. Zero-Cost Abstractions with PyO3

4.1 The Functional Pipeline in Python

Polarway exposes Rust abstractions to Python via PyO3. The Python code looks like idiomatic Python; the Rust compiler inlines and vectorises it.

import polarway as pw

# The Python side: clean, functional, readable
result = (
    pw.read_csv("ohlcv_btc.csv")
    .and_then(pw.validate_ohlcv_schema)     # Returns Result
    .and_then(pw.clean_missing_values)      # Returns Result
    .and_then(pw.apply_log_returns)         # Returns Result
    .map(pw.compute_rolling_sharpe)         # Returns Result (infallible step)
    .map_err(lambda e: f"Pipeline failed: {e}")
)

# Pattern-match the result
if result.is_ok():
    df = result.unwrap()
    print(f"Sharpe: {df['sharpe'].mean():.3f}")
else:
    print(f"Error: {result.err_value()}")

4.2 Performance Comparison

Zero-Cost Abstractions — Compile-Time Elimination
Python Functional Code
df.map(fn).filter(pred).map(g)
Rust/PyO3 boundary

compile
Optimised Machine Code
SIMD vectorised loop
No heap alloc, no boxing
No iterator overhead
10–100× faster than Pandas

Rust’s zero-cost abstraction rule states that high-level abstractions (functors, monads, iterators) produce the same machine code as a hand-optimised low-level implementation — verifiable with cargo asm or the Compiler Explorer.

5. Mathematical Foundations

5.1 Functor Laws

A functor \(F\) is a structure-preserving mapping. The map operation on Result must satisfy the functor laws:

$$\text{Identité :} \quad F(\text{id}) = \text{id}_F \qquad \Leftrightarrow \qquad \texttt{result.map(|x| x) == result}$$ $$\text{Composition :} \quad F(g \circ f) = F(g) \circ F(f) \qquad \Leftrightarrow \qquad \texttt{result.map(|x| g(f(x))) == result.map(f).map(g)}$$

5.2 Monad Laws

Result<T, E> forms a monad with Ok as unit and and_then as bind (\(\gg\!\!=\)). The three monad laws:

$$\text{Identité gauche :} \quad \texttt{Ok}(a) \gg\!\!= f \equiv f(a)$$ $$\text{Identité droite :} \quad m \gg\!\!= \texttt{Ok} \equiv m$$ $$\text{Associativité :} \quad (m \gg\!\!= f) \gg\!\!= g \equiv m \gg\!\!= (\lambda x.\; f(x) \gg\!\!= g)$$

These laws ensure that pipeline composition is predictable and refactorable: grouping or reordering steps that satisfy the monad laws does not change the semantics of the pipeline.

6. Polarway Pipelines in Practice

6.1 HFT Pipeline Architecture

A complete HFT data processing pipeline in Polarway combines data reading, validation, signal computation, and error handling in a composable chain:

HFT Data Pipeline with Railway Error Handling
Data Sources: CCXT · CLOB · DeFi WebSocket
1. pw.read_streaming(feeds)Result<Stream, IoError>
and_then
2. validate_ohlcv_schema()Result<Stream, SchemaError>
and_then
3. compute_log_returns()Result<DataFrame, ComputeError>
map (infallible)
4. apply_signal_pipeline()DataFrame
and_then
Order signals emitted → HFThot execution engine
Any step returning Err → entire chain bypassed → logged & handled gracefully
import polarway as pw

def run_signal_pipeline(feeds: list) -> pw.Result:
    return (
        pw.read_streaming(feeds)
        .and_then(pw.validate_ohlcv_schema)
        .and_then(pw.clean_missing_values)
        .and_then(lambda df: pw.compute_log_returns(df, periods=[1, 5, 20]))
        .map(pw.apply_bollinger_bands)
        .map(pw.compute_rsi)
        .and_then(pw.filter_by_liquidity_threshold)
        .map(pw.rank_signals_by_strength)
        .map_err(lambda e: pw.log_error(e, context="signal_pipeline"))
    )

result = run_signal_pipeline(live_feeds)
result.match_result(
    on_ok=lambda df: execution_engine.submit(df),
    on_err=lambda e: alert_system.notify(str(e))
)

6.2 Stream Processing with Functors

For continuous data streams (tick data, orderbook updates), Polarway offers a lazily-evaluated stream model, compiled to Rust:

import polarway as pw

# Stream processing: lazily evaluated, zero-copy, SIMD-vectorised
stream = (
    pw.read_parquet_streaming("ticks/*.parquet")
    .map(lambda batch: batch.select(["timestamp", "price", "volume"]))
    .filter(lambda batch: (batch["volume"] > 0).all())
    .flat_map(lambda batch: batch.resample("5min"))
    .take(10_000)  # Lazy — nothing executes until this
)

# Fold (functional reduce) — single Rust kernel
total_volume = stream.fold(
    initial=0.0,
    fn=lambda acc, batch: acc + batch["volume"].sum()
)
print(f"Total volume (5-min): {total_volume:,.0f}")

7. References

  1. Wlaschin, S. (2014). Railway-Oriented Programming. fsharpforfunandprofit.com.
  2. Klabnik, S. & Nichols, C. (2023). The Rust Programming Language, Chapter 9: Error Handling.
  3. Wadler, P. (1992). The Essence of Functional Programming. POPL ’92.
  4. Moggi, E. (1991). Notions of Computation and Monads. Information and Computation, 93(1), 55–92.
  5. Pyo3 Contributors. (2024). PyO3 User Guide. pyo3.rs.
  6. Polarway Project. (2026). github.com/ThotDjehuty/polarway.

🚀 Try Polarway

Polarway is open-source. Explore Railway-Oriented pipelines on our live demo.

Launch Demo GitHub View Articles