Railway Oriented Programming (ROP) library for Clojure - A library for concise and elegant error handling.
railway-clj is a Clojure implementation of the Railway Oriented Programming (ROP) pattern from functional programming. This pattern explicitly distinguishes between "happy path" and "error path" flows, making it particularly useful for building pipelines that perform sequential value transformations.
Key Features:
Add the following to your Leiningen project's :dependencies
:
[railway-clj "0.1.0"]
The core of railway-clj is the Success/Failure pattern that allows you to chain operations while automatically handling errors:
(ns example.core
(:require [railway-clj.core :as r]))
;; Threading success values through a pipeline
(r/|> (r/success 5)
inc
#(* % 2))
;; => #railway_clj.core.Success{:value 12}
;; Error handling with pattern matching
(r/>-< (r/|> (r/success 5)
inc
#(* % 2))
#(str "Result: " %)
#(str "Error: " (:error %)))
;; => "Result: 12"
;; When an error occurs, it short-circuits the pipeline
(r/|> (r/failure {:error "Invalid input"})
inc
#(* % 2))
;; => #railway_clj.core.Failure{:error {:error "Invalid input"}}
;; Map over collections with error handling
(r/map-success #(/ 10 %) [1 2 5 10])
;; => (#railway_clj.core.Success{:value 10}
;; #railway_clj.core.Success{:value 5}
;; #railway_clj.core.Success{:value 2}
;; #railway_clj.core.Success{:value 1})
;; Validate and transform data
(defn validate-positive [x]
(if (pos? x)
(r/success x)
(r/failure {:error "Must be positive" :value x})))
(r/|> (r/success -5)
validate-positive
#(* % 2))
;; => #railway_clj.core.Failure{:error {:error "Must be positive", :value -5}}
railway-clj integrates seamlessly with core.async for asynchronous workflows:
(ns example.async
(:require [railway-clj.async :as ra]
[railway-clj.core :as r]
[clojure.core.async :refer [<! go chan >!]]))
(defn async-increment [x]
(go (r/success (inc x))))
(defn async-multiply [x]
(go (r/success (* x 2))))
;; Async pipeline
(go
(let [result (<! (ra/|> (go (r/success 5))
async-increment
async-multiply))]
(println result)))
;; => #railway_clj.core.Success{:value 12}
;; Error handling in async context
(defn async-divide [x y]
(go
(if (zero? y)
(r/failure {:error "Division by zero"})
(r/success (/ x y)))))
(go
(let [result (<! (ra/|> (go (r/success 10))
#(async-divide % 0)))]
(println result)))
;; => #railway_clj.core.Failure{:error {:error "Division by zero"}}
Protect your application from cascading failures with the circuit breaker pattern:
(ns example.circuit-breaker
(:require [railway-clj.circuit-breaker :as cb]
[railway-clj.core :as r]))
;; Create a circuit breaker
(def breaker (cb/create-circuit-breaker
{:failure-threshold 3 ; Open after 3 failures
:reset-timeout-ms 5000 ; Try again after 5 seconds
:half-open-calls 1})) ; Test with 1 call when half-open
;; Wrap external service calls
(defn external-api-call [url]
(try
;; Simulate HTTP call
(let [response {:status 200 :body "OK"}]
(if (>= (:status response) 400)
(r/failure {:error "HTTP error" :status (:status response)})
(r/success response)))
(catch Exception e
(r/failure {:error "Connection error" :message (.getMessage e)}))))
(def protected-call (breaker external-api-call))
;; Use the protected call
(protected-call "https://api.example.com/data")
;; => #railway_clj.core.Success{:value {:status 200, :body "OK"}}
;; When circuit is open, calls fail fast
;; #railway_clj.core.Failure{:error {:error "Circuit breaker is open"}}
Add resilience with configurable retry logic:
(ns example.retry
(:require [railway-clj.retry :as retry]
[railway-clj.core :as r]))
;; Create a retrying function
(def resilient-api-call
(retry/retry
(fn [url]
(try
;; Simulate unreliable network call
(if (< (rand) 0.7) ; 70% failure rate
(r/failure {:error "Network timeout"})
(r/success {:data "Important data"}))
(catch Exception e
(r/failure {:error "Unexpected error" :message (.getMessage e)}))))
{:max-attempts 5 ; Try up to 5 times
:backoff-ms 1000 ; Start with 1 second delay
:backoff-factor 2 ; Double the delay each time
:retryable? retry/retryable-error?}))
;; Use the resilient function
(resilient-api-call "https://unreliable-api.com/data")
;; Will retry on failures with exponential backoff
;; => #railway_clj.core.Success{:value {:data "Important data"}}
You can combine all these patterns for robust error handling:
(ns example.combined
(:require [railway-clj.core :as r]
[railway-clj.async :as ra]
[railway-clj.circuit-breaker :as cb]
[railway-clj.retry :as retry]
[clojure.core.async :refer [go <!]]))
;; Create a fully protected async service call
(def protected-service
(-> (fn [request]
(go
;; Your actual service logic here
(if (valid? request)
(r/success (process-request request))
(r/failure {:error "Invalid request"}))))
(retry/retry {:max-attempts 3 :backoff-ms 500})
(cb/create-circuit-breaker {:failure-threshold 5 :reset-timeout-ms 10000})))
;; Use in an async pipeline
(go
(let [result (<! (ra/|> (go (r/success {:user-id 123}))
protected-service
#(go (r/success (format-response %)))))]
(r/>-< result
#(println "Success:" %)
#(println "Failed:" (:error %)))))
success
- Create a success valuefailure
- Create a failure value|>
- Pipeline operator for chaining operations>-<
- Pattern match on success/failuremap-success
- Map function over collection, handling errorsra/|>
- Async pipeline operatorra/>-<
- Async pattern matchingcreate-circuit-breaker
- Create a circuit breaker with configuration:closed
, :open
, :half-open
retry
- Wrap function with retry logicretryable-error?
- Default predicate for retryable errorsRun the test suite:
lein test
Run with coverage (requires 70% minimum):
lein cloverage
Current test coverage: 86.28% (Forms) / 96.79% (Lines)
Coverage breakdown by namespace:
railway-clj.core
: 99.27% forms, 96.70% linesrailway-clj.retry
: 100.00% forms, 100.00% linesrailway-clj.async
: 80.60% forms, 91.67% linesrailway-clj.circuit-breaker
: 79.68% forms, 100.00% linesMIT License - see LICENSE file for details.
Can you improve this documentation? These fine people already did:
konkon & Takayuki-Y5991Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close