Your First Real Program
Let's build something complete: a small program that loads configuration, connects to a database, queries a user, and formats a greeting. It's simple enough to fit on one page, but real enough to demonstrate the full effect workflow.
The Domain
#![allow(unused)] fn main() { #[derive(Debug)] struct Config { db_url: String, app_name: String, } #[derive(Debug)] struct User { id: u64, name: String, email: String, } #[derive(Debug)] enum AppError { Config(String), Database(String), } }
The Individual Steps
Each step is a focused effect:
#![allow(unused)] fn main() { use id_effect::{Effect, effect, succeed, fail}; fn load_config() -> Effect<Config, AppError, ()> { // In a real app, read from a file or env vars succeed(Config { db_url: "postgres://localhost/myapp".to_string(), app_name: "Greeter".to_string(), }) } fn connect_db(config: &Config) -> Effect<Database, AppError, ()> { Database::connect(&config.db_url) .map_error(|e| AppError::Database(format!("connect: {e}"))) } fn fetch_user(db: &Database, id: u64) -> Effect<User, AppError, ()> { db.query_user(id) .map_error(|e| AppError::Database(format!("query: {e}"))) } fn format_greeting(config: &Config, user: &User) -> String { format!("{}: Hello, {}! ({})", config.app_name, user.name, user.email) } }
Composing the Program
Now we compose these steps into one effect using effect!:
#![allow(unused)] fn main() { fn greet_user(user_id: u64) -> Effect<String, AppError, ()> { effect! { let config = ~ load_config(); let db = ~ connect_db(&config); let user = ~ fetch_user(&db, user_id); format_greeting(&config, &user) } } }
Read it like a recipe:
- Load config — if it fails, stop with
AppError::Config - Connect to DB — if it fails, stop with
AppError::Database - Fetch user — if it fails, stop with
AppError::Database - Format the greeting — this is pure, always succeeds
Nothing has run yet. greet_user(42) is a value.
Running It
At the edge of the program — in main — we execute:
fn main() { match run_blocking(greet_user(42)) { Ok(greeting) => println!("{greeting}"), Err(AppError::Config(msg)) => eprintln!("Config error: {msg}"), Err(AppError::Database(msg)) => eprintln!("DB error: {msg}"), } }
Testing It
Because the effect is a description, testing is straightforward — just swap out the underlying steps:
#![allow(unused)] fn main() { #[test] fn test_greeting_format() { let effect = effect! { let config = ~ succeed(Config { db_url: "unused".into(), app_name: "TestApp".into(), }); let user = ~ succeed(User { id: 1, name: "Alice".into(), email: "alice@example.com".into(), }); format_greeting(&config, &user) }; let result = run_test(effect); assert_eq!(result.unwrap(), "TestApp: Hello, Alice! (alice@example.com)"); } }
No mocking framework. No Arc<dyn Trait> plumbing. Just substitute different succeed values for the steps you want to control.
What You Just Learned
You've written a complete effect-based program. Along the way you used:
succeedandfailto construct effects from values.mapand.map_errorto transform success and error typeseffect! { ~ ... }to sequence effects without callback nestingrun_blockingto execute at the program edgerun_testto verify behaviour in tests
That's the core of 90% of what you'll write day-to-day. The next two chapters go deeper: Chapter 3 explores the effect! macro in detail, and Chapter 4 begins the tour of R — the environment type that makes dependency injection a compile-time guarantee.
You just wrote your first effect-based program. It won't be your last.