Skip to main content

functype_io/
io.rs

1use std::fmt;
2
3use anyhow;
4use functype_core::either::Either;
5use functype_core::pure::Pure;
6
7/// A lazy, composable IO effect type.
8///
9/// `IO<A>` represents a computation that, when executed, performs side effects
10/// and produces a value of type `A` (or fails with an error).
11///
12/// Nothing executes until `.run()` is called — this gives referential transparency,
13/// deferred execution, and composable effect descriptions.
14///
15/// # Design
16///
17/// - **One type parameter**: `IO<A>` keeps signatures clean. Errors are type-erased
18///   via `anyhow::Error` and surfaced at `.run()` boundaries. For typed errors,
19///   use `IO<Result<A, E>>` — opt-in, not mandatory.
20/// - **`Send + 'static`**: Compatible with tokio and thread pools.
21/// - **`FnOnce`**: Consumed exactly once.
22///
23/// # Examples
24///
25/// ```
26/// use functype_io::IO;
27///
28/// let io = IO::succeed(42);
29/// assert_eq!(io.run().unwrap(), 42);
30///
31/// let io = IO::effect(|| 1 + 2).map(|x| x * 10);
32/// assert_eq!(io.run().unwrap(), 30);
33/// ```
34pub struct IO<A> {
35    thunk: Box<dyn FnOnce() -> Result<A, anyhow::Error> + Send + 'static>,
36}
37
38impl<A: fmt::Debug> fmt::Debug for IO<A> {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        f.debug_struct("IO").finish_non_exhaustive()
41    }
42}
43
44/// Alias for `IO<A>`.
45pub type Task<A> = IO<A>;
46
47// ============================================================
48// Constructors
49// ============================================================
50
51impl<A: Send + 'static> IO<A> {
52    /// Creates an IO that succeeds with the given value.
53    pub fn succeed(a: A) -> IO<A> {
54        IO {
55            thunk: Box::new(move || Ok(a)),
56        }
57    }
58
59    /// Creates an IO that fails with the given error.
60    pub fn fail(e: impl Into<anyhow::Error> + Send + 'static) -> IO<A> {
61        IO {
62            thunk: Box::new(move || Err(e.into())),
63        }
64    }
65
66    /// Creates an IO from an infallible side effect.
67    pub fn effect(f: impl FnOnce() -> A + Send + 'static) -> IO<A> {
68        IO {
69            thunk: Box::new(move || Ok(f())),
70        }
71    }
72
73    /// Creates an IO from a fallible side effect (primary constructor).
74    pub fn effect_result(f: impl FnOnce() -> Result<A, anyhow::Error> + Send + 'static) -> IO<A> {
75        IO { thunk: Box::new(f) }
76    }
77
78    /// Lifts a `Result` into an `IO`.
79    pub fn from_result(result: Result<A, impl Into<anyhow::Error> + Send + 'static>) -> IO<A> {
80        IO {
81            thunk: Box::new(move || result.map_err(Into::into)),
82        }
83    }
84
85    /// Lifts an `Option` into an `IO`, using the provided closure for the `None` case.
86    pub fn from_option(option: Option<A>, on_none: impl FnOnce() -> anyhow::Error + Send + 'static) -> IO<A> {
87        IO {
88            thunk: Box::new(move || option.ok_or_else(on_none)),
89        }
90    }
91
92    /// Lifts an `Either` into an `IO`. `Left` becomes the error, `Right` becomes the value.
93    pub fn from_either(either: Either<impl Into<anyhow::Error> + Send + 'static, A>) -> IO<A> {
94        IO {
95            thunk: Box::new(move || match either {
96                Either::Left(e) => Err(e.into()),
97                Either::Right(a) => Ok(a),
98            }),
99        }
100    }
101}
102
103// ============================================================
104// Execution
105// ============================================================
106
107impl<A> IO<A> {
108    /// Consumes and executes this IO, returning the result.
109    pub fn run(self) -> Result<A, anyhow::Error> {
110        (self.thunk)()
111    }
112
113    /// Executes this IO, panicking on error.
114    pub fn run_or_panic(self) -> A
115    where
116        A: fmt::Debug,
117    {
118        self.run().expect("IO::run_or_panic failed")
119    }
120}
121
122// ============================================================
123// Monadic combinators
124// ============================================================
125
126impl<A: Send + 'static> IO<A> {
127    /// Transforms the success value.
128    pub fn map<B: Send + 'static>(self, f: impl FnOnce(A) -> B + Send + 'static) -> IO<B> {
129        IO {
130            thunk: Box::new(move || (self.thunk)().map(f)),
131        }
132    }
133
134    /// Chains a computation that depends on the success value.
135    pub fn flat_map<B: Send + 'static>(self, f: impl FnOnce(A) -> IO<B> + Send + 'static) -> IO<B> {
136        IO {
137            thunk: Box::new(move || {
138                let a = (self.thunk)()?;
139                f(a).run()
140            }),
141        }
142    }
143
144    /// Alias for `flat_map` (for `fdo!` macro compatibility).
145    pub fn and_then<B: Send + 'static>(self, f: impl FnOnce(A) -> IO<B> + Send + 'static) -> IO<B> {
146        self.flat_map(f)
147    }
148
149    /// Combines two IOs into a tuple.
150    pub fn zip<B: Send + 'static>(self, other: IO<B>) -> IO<(A, B)> {
151        IO {
152            thunk: Box::new(move || {
153                let a = (self.thunk)()?;
154                let b = (other.thunk)()?;
155                Ok((a, b))
156            }),
157        }
158    }
159
160    /// Combines two IOs with a function.
161    pub fn zip_with<B: Send + 'static, C: Send + 'static>(
162        self,
163        other: IO<B>,
164        f: impl FnOnce(A, B) -> C + Send + 'static,
165    ) -> IO<C> {
166        IO {
167            thunk: Box::new(move || {
168                let a = (self.thunk)()?;
169                let b = (other.thunk)()?;
170                Ok(f(a, b))
171            }),
172        }
173    }
174
175    /// Sequences two IOs, discarding the first result.
176    pub fn then<B: Send + 'static>(self, other: IO<B>) -> IO<B> {
177        IO {
178            thunk: Box::new(move || {
179                let _a = (self.thunk)()?;
180                (other.thunk)()
181            }),
182        }
183    }
184
185    /// Runs a side effect on the success value, returning the original value.
186    pub fn tap(self, f: impl FnOnce(&A) + Send + 'static) -> IO<A> {
187        IO {
188            thunk: Box::new(move || {
189                let a = (self.thunk)()?;
190                f(&a);
191                Ok(a)
192            }),
193        }
194    }
195}
196
197// ============================================================
198// Error handling
199// ============================================================
200
201impl<A: Send + 'static> IO<A> {
202    /// Transforms the error.
203    pub fn map_error(self, f: impl FnOnce(anyhow::Error) -> anyhow::Error + Send + 'static) -> IO<A> {
204        IO {
205            thunk: Box::new(move || (self.thunk)().map_err(f)),
206        }
207    }
208
209    /// Recovers from an error by producing a new IO.
210    pub fn catch(self, f: impl FnOnce(anyhow::Error) -> IO<A> + Send + 'static) -> IO<A> {
211        IO {
212            thunk: Box::new(move || match (self.thunk)() {
213                Ok(a) => Ok(a),
214                Err(e) => f(e).run(),
215            }),
216        }
217    }
218
219    /// Falls back to another IO on error.
220    pub fn or_else(self, other: IO<A>) -> IO<A> {
221        IO {
222            thunk: Box::new(move || match (self.thunk)() {
223                Ok(a) => Ok(a),
224                Err(_) => (other.thunk)(),
225            }),
226        }
227    }
228
229    /// Makes this IO infallible by wrapping the result in `Either`.
230    /// Errors become `Left`, successes become `Right`.
231    pub fn either(self) -> IO<Either<anyhow::Error, A>> {
232        IO {
233            thunk: Box::new(move || {
234                Ok(match (self.thunk)() {
235                    Ok(a) => Either::Right(a),
236                    Err(e) => Either::Left(e),
237                })
238            }),
239        }
240    }
241
242    /// Retries a factory function up to `n` times on failure.
243    ///
244    /// This is an associated function (not a method) because IO is `FnOnce` —
245    /// the factory creates a fresh IO on each attempt.
246    pub fn retry(factory: impl Fn() -> IO<A> + Send + 'static, n: usize) -> IO<A> {
247        IO {
248            thunk: Box::new(move || {
249                let mut last_err = None;
250                for _ in 0..=n {
251                    match factory().run() {
252                        Ok(a) => return Ok(a),
253                        Err(e) => last_err = Some(e),
254                    }
255                }
256                Err(last_err.unwrap())
257            }),
258        }
259    }
260}
261
262// ============================================================
263// Resource safety
264// ============================================================
265
266impl IO<()> {
267    /// Acquires a resource, uses it, and guarantees release even on failure.
268    ///
269    /// - `acquire`: IO that produces the resource
270    /// - `use_fn`: Function that uses the resource (receives a reference)
271    /// - `release`: Cleanup function that always runs after `use_fn` (on both success and error)
272    ///
273    /// If `acquire` fails, neither `use_fn` nor `release` run.
274    pub fn bracket<R: Send + 'static, B: Send + 'static>(
275        acquire: IO<R>,
276        use_fn: impl FnOnce(&R) -> IO<B> + Send + 'static,
277        release: impl FnOnce(&R) + Send + 'static,
278    ) -> IO<B> {
279        IO {
280            thunk: Box::new(move || {
281                let resource = acquire.run()?;
282                let result = use_fn(&resource).run();
283                release(&resource);
284                result
285            }),
286        }
287    }
288}
289
290impl<A: Send + 'static> IO<A> {
291    /// Guarantees that the finalizer runs after this IO, regardless of success or failure.
292    pub fn ensuring(self, finalizer: impl FnOnce() + Send + 'static) -> IO<A> {
293        IO {
294            thunk: Box::new(move || {
295                let result = (self.thunk)();
296                finalizer();
297                result
298            }),
299        }
300    }
301}
302
303// ============================================================
304// Async interop
305// ============================================================
306
307impl<A: Send + 'static> IO<A> {
308    /// Creates an IO from a future. Blocks on the current tokio runtime.
309    ///
310    /// Must be called from within a tokio runtime context. Uses `block_in_place`
311    /// to avoid panicking when called from an async context (requires multi-thread runtime).
312    pub fn from_future(future: impl std::future::Future<Output = Result<A, anyhow::Error>> + Send + 'static) -> IO<A> {
313        IO {
314            thunk: Box::new(move || {
315                let handle = tokio::runtime::Handle::current();
316                tokio::task::block_in_place(|| handle.block_on(future))
317            }),
318        }
319    }
320
321    /// Converts this IO into a future.
322    pub async fn to_future(self) -> Result<A, anyhow::Error> {
323        self.run()
324    }
325}
326
327// ============================================================
328// Pure trait
329// ============================================================
330
331impl<A: Send + 'static> Pure<A> for IO<A> {
332    fn pure(a: A) -> Self {
333        IO::succeed(a)
334    }
335}
336
337// ============================================================
338// Tests
339// ============================================================
340
341#[cfg(test)]
342mod tests {
343    use super::*;
344    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
345    use std::sync::Arc;
346
347    // ===== Step 1: Core type + constructors + execution =====
348
349    #[test]
350    fn succeed_returns_value() {
351        let io = IO::succeed(42);
352        assert_eq!(io.run().unwrap(), 42);
353    }
354
355    #[test]
356    fn succeed_with_string() {
357        let io = IO::succeed("hello".to_string());
358        assert_eq!(io.run().unwrap(), "hello");
359    }
360
361    #[test]
362    fn fail_returns_error() {
363        let io: IO<i32> = IO::fail(anyhow::anyhow!("boom"));
364        let err = io.run().unwrap_err();
365        assert_eq!(err.to_string(), "boom");
366    }
367
368    #[test]
369    fn effect_captures_side_effect() {
370        let counter = Arc::new(AtomicUsize::new(0));
371        let c = counter.clone();
372        let io = IO::effect(move || {
373            c.fetch_add(1, Ordering::SeqCst);
374            42
375        });
376        assert_eq!(counter.load(Ordering::SeqCst), 0, "effect should be lazy");
377        assert_eq!(io.run().unwrap(), 42);
378        assert_eq!(counter.load(Ordering::SeqCst), 1, "effect should have run");
379    }
380
381    #[test]
382    fn laziness_verified() {
383        let ran = Arc::new(AtomicBool::new(false));
384        let r = ran.clone();
385        let _io = IO::effect(move || {
386            r.store(true, Ordering::SeqCst);
387        });
388        assert!(!ran.load(Ordering::SeqCst), "should not run until .run()");
389    }
390
391    #[test]
392    fn effect_result_success() {
393        let io = IO::effect_result(|| Ok(42));
394        assert_eq!(io.run().unwrap(), 42);
395    }
396
397    #[test]
398    fn effect_result_failure() {
399        let io: IO<i32> = IO::effect_result(|| Err(anyhow::anyhow!("fail")));
400        assert!(io.run().is_err());
401    }
402
403    #[test]
404    fn from_result_ok() {
405        let io = IO::from_result(Ok::<i32, anyhow::Error>(42));
406        assert_eq!(io.run().unwrap(), 42);
407    }
408
409    #[test]
410    fn from_result_err() {
411        let io = IO::from_result(Err::<i32, _>(anyhow::anyhow!("nope")));
412        assert!(io.run().is_err());
413    }
414
415    #[test]
416    fn from_option_some() {
417        let io = IO::from_option(Some(42), || anyhow::anyhow!("missing"));
418        assert_eq!(io.run().unwrap(), 42);
419    }
420
421    #[test]
422    fn from_option_none() {
423        let io = IO::from_option(None::<i32>, || anyhow::anyhow!("missing"));
424        let err = io.run().unwrap_err();
425        assert_eq!(err.to_string(), "missing");
426    }
427
428    #[test]
429    fn from_either_right() {
430        let either: Either<anyhow::Error, i32> = Either::Right(42);
431        let io = IO::from_either(either);
432        assert_eq!(io.run().unwrap(), 42);
433    }
434
435    #[test]
436    fn from_either_left() {
437        let either: Either<anyhow::Error, i32> = Either::Left(anyhow::anyhow!("left error"));
438        let io = IO::from_either(either);
439        assert_eq!(io.run().unwrap_err().to_string(), "left error");
440    }
441
442    #[test]
443    fn run_or_panic_success() {
444        let io = IO::succeed(42);
445        assert_eq!(io.run_or_panic(), 42);
446    }
447
448    #[test]
449    #[should_panic(expected = "IO::run_or_panic failed")]
450    fn run_or_panic_failure() {
451        let io: IO<i32> = IO::fail(anyhow::anyhow!("boom"));
452        io.run_or_panic();
453    }
454
455    // ===== Step 2: Monadic combinators =====
456
457    #[test]
458    fn map_transforms_value() {
459        let io = IO::succeed(21).map(|x| x * 2);
460        assert_eq!(io.run().unwrap(), 42);
461    }
462
463    #[test]
464    fn map_preserves_error() {
465        let io: IO<i32> = IO::<i32>::fail(anyhow::anyhow!("err")).map(|x| x * 2);
466        assert_eq!(io.run().unwrap_err().to_string(), "err");
467    }
468
469    #[test]
470    fn flat_map_chains() {
471        let io = IO::succeed(10).flat_map(|x| IO::succeed(x + 5));
472        assert_eq!(io.run().unwrap(), 15);
473    }
474
475    #[test]
476    fn flat_map_short_circuits_on_first_error() {
477        let ran = Arc::new(AtomicBool::new(false));
478        let r = ran.clone();
479        let io: IO<i32> = IO::fail(anyhow::anyhow!("first")).flat_map(move |x| {
480            r.store(true, Ordering::SeqCst);
481            IO::succeed(x)
482        });
483        assert!(io.run().is_err());
484        assert!(!ran.load(Ordering::SeqCst));
485    }
486
487    #[test]
488    fn flat_map_short_circuits_on_second_error() {
489        let io = IO::succeed(10).flat_map(|_| IO::<i32>::fail(anyhow::anyhow!("second")));
490        assert_eq!(io.run().unwrap_err().to_string(), "second");
491    }
492
493    #[test]
494    fn and_then_is_flat_map_alias() {
495        let io = IO::succeed(10).and_then(|x| IO::succeed(x + 1));
496        assert_eq!(io.run().unwrap(), 11);
497    }
498
499    #[test]
500    fn zip_combines_two_ios() {
501        let io = IO::succeed(1).zip(IO::succeed(2));
502        assert_eq!(io.run().unwrap(), (1, 2));
503    }
504
505    #[test]
506    fn zip_short_circuits_first() {
507        let io = IO::<i32>::fail(anyhow::anyhow!("first")).zip(IO::succeed(2));
508        assert!(io.run().is_err());
509    }
510
511    #[test]
512    fn zip_short_circuits_second() {
513        let io = IO::succeed(1).zip(IO::<i32>::fail(anyhow::anyhow!("second")));
514        assert!(io.run().is_err());
515    }
516
517    #[test]
518    fn zip_with_combines_with_function() {
519        let io = IO::succeed(10).zip_with(IO::succeed(20), |a, b| a + b);
520        assert_eq!(io.run().unwrap(), 30);
521    }
522
523    #[test]
524    fn then_discards_first() {
525        let io = IO::succeed(1).then(IO::succeed(2));
526        assert_eq!(io.run().unwrap(), 2);
527    }
528
529    #[test]
530    fn then_short_circuits_on_first_error() {
531        let io = IO::<i32>::fail(anyhow::anyhow!("err")).then(IO::succeed(2));
532        assert!(io.run().is_err());
533    }
534
535    #[test]
536    fn tap_runs_side_effect() {
537        let seen = Arc::new(AtomicUsize::new(0));
538        let s = seen.clone();
539        let io = IO::succeed(42).tap(move |x| {
540            s.store(*x as usize, Ordering::SeqCst);
541        });
542        assert_eq!(io.run().unwrap(), 42);
543        assert_eq!(seen.load(Ordering::SeqCst), 42);
544    }
545
546    #[test]
547    fn tap_does_not_run_on_error() {
548        let ran = Arc::new(AtomicBool::new(false));
549        let r = ran.clone();
550        let io: IO<i32> = IO::fail(anyhow::anyhow!("err")).tap(move |_| {
551            r.store(true, Ordering::SeqCst);
552        });
553        assert!(io.run().is_err());
554        assert!(!ran.load(Ordering::SeqCst));
555    }
556
557    // Monad laws
558
559    #[test]
560    fn monad_left_identity() {
561        // pure(a).flat_map(f) == f(a)
562        let f = |x: i32| IO::succeed(x + 1);
563        let left = IO::succeed(10).flat_map(f);
564        let right = f(10);
565        assert_eq!(left.run().unwrap(), right.run().unwrap());
566    }
567
568    #[test]
569    fn monad_right_identity() {
570        // m.flat_map(pure) == m
571        let m = IO::succeed(42);
572        let result = IO::succeed(42).flat_map(IO::succeed);
573        assert_eq!(m.run().unwrap(), result.run().unwrap());
574    }
575
576    #[test]
577    fn monad_associativity() {
578        // (m.flat_map(f)).flat_map(g) == m.flat_map(|x| f(x).flat_map(g))
579        let f = |x: i32| IO::succeed(x + 1);
580        let g = |x: i32| IO::succeed(x * 2);
581
582        let left = IO::succeed(10).flat_map(f).flat_map(g);
583        let f2 = |x: i32| IO::succeed(x + 1);
584        let g2 = |x: i32| IO::succeed(x * 2);
585        let right = IO::succeed(10).flat_map(move |x| f2(x).flat_map(g2));
586        assert_eq!(left.run().unwrap(), right.run().unwrap());
587    }
588
589    // ===== Step 3: Error handling =====
590
591    #[test]
592    fn map_error_transforms_error() {
593        let io: IO<i32> = IO::fail(anyhow::anyhow!("original")).map_error(|_| anyhow::anyhow!("transformed"));
594        assert_eq!(io.run().unwrap_err().to_string(), "transformed");
595    }
596
597    #[test]
598    fn map_error_preserves_success() {
599        let io = IO::succeed(42).map_error(|_| anyhow::anyhow!("nope"));
600        assert_eq!(io.run().unwrap(), 42);
601    }
602
603    #[test]
604    fn catch_recovers_from_error() {
605        let io: IO<i32> = IO::fail(anyhow::anyhow!("err")).catch(|_| IO::succeed(99));
606        assert_eq!(io.run().unwrap(), 99);
607    }
608
609    #[test]
610    fn catch_preserves_success() {
611        let io = IO::succeed(42).catch(|_| IO::succeed(99));
612        assert_eq!(io.run().unwrap(), 42);
613    }
614
615    #[test]
616    fn or_else_falls_back() {
617        let io: IO<i32> = IO::fail(anyhow::anyhow!("err")).or_else(IO::succeed(99));
618        assert_eq!(io.run().unwrap(), 99);
619    }
620
621    #[test]
622    fn or_else_preserves_success() {
623        let io = IO::succeed(42).or_else(IO::succeed(99));
624        assert_eq!(io.run().unwrap(), 42);
625    }
626
627    #[test]
628    fn either_wraps_success() {
629        let io = IO::succeed(42).either();
630        let result = io.run().unwrap();
631        assert!(result.is_right());
632        assert_eq!(result.right_value(), Some(&42));
633    }
634
635    #[test]
636    fn either_wraps_error() {
637        let io: IO<i32> = IO::fail(anyhow::anyhow!("err"));
638        let result = io.either().run().unwrap();
639        assert!(result.is_left());
640        let msg = result.fold(|e| e.to_string(), |_| String::new());
641        assert_eq!(msg, "err");
642    }
643
644    #[test]
645    fn retry_succeeds_on_nth_attempt() {
646        let counter = Arc::new(AtomicUsize::new(0));
647        let c = counter.clone();
648        let io = IO::retry(
649            move || {
650                let count = c.fetch_add(1, Ordering::SeqCst);
651                if count < 2 {
652                    IO::fail(anyhow::anyhow!("not yet"))
653                } else {
654                    IO::succeed(42)
655                }
656            },
657            3,
658        );
659        assert_eq!(io.run().unwrap(), 42);
660        assert_eq!(counter.load(Ordering::SeqCst), 3);
661    }
662
663    #[test]
664    fn retry_exhausts_attempts() {
665        let counter = Arc::new(AtomicUsize::new(0));
666        let c = counter.clone();
667        let io: IO<i32> = IO::retry(
668            move || {
669                c.fetch_add(1, Ordering::SeqCst);
670                IO::fail(anyhow::anyhow!("always fails"))
671            },
672            2,
673        );
674        assert!(io.run().is_err());
675        assert_eq!(counter.load(Ordering::SeqCst), 3); // initial + 2 retries
676    }
677
678    // ===== Step 4: Resource safety =====
679
680    #[test]
681    fn bracket_runs_all_three() {
682        let order = Arc::new(std::sync::Mutex::new(Vec::new()));
683        let o1 = order.clone();
684        let o2 = order.clone();
685        let o3 = order.clone();
686
687        let io = IO::bracket(
688            IO::effect(move || {
689                o1.lock().unwrap().push("acquire");
690                "resource"
691            }),
692            move |r| {
693                o2.lock().unwrap().push("use");
694                IO::succeed(r.len())
695            },
696            move |_r| {
697                o3.lock().unwrap().push("release");
698            },
699        );
700
701        assert_eq!(io.run().unwrap(), 8); // "resource".len()
702        assert_eq!(*order.lock().unwrap(), vec!["acquire", "use", "release"]);
703    }
704
705    #[test]
706    fn bracket_releases_on_use_error() {
707        let released = Arc::new(AtomicBool::new(false));
708        let r = released.clone();
709
710        let io = IO::bracket(
711            IO::succeed(42),
712            |_| IO::<i32>::fail(anyhow::anyhow!("use failed")),
713            move |_| {
714                r.store(true, Ordering::SeqCst);
715            },
716        );
717
718        assert!(io.run().is_err());
719        assert!(released.load(Ordering::SeqCst), "release should run on use error");
720    }
721
722    #[test]
723    fn bracket_does_not_release_on_acquire_error() {
724        let released = Arc::new(AtomicBool::new(false));
725        let r = released.clone();
726
727        let io = IO::bracket(
728            IO::<i32>::fail(anyhow::anyhow!("acquire failed")),
729            |_| IO::succeed(0),
730            move |_| {
731                r.store(true, Ordering::SeqCst);
732            },
733        );
734
735        assert!(io.run().is_err());
736        assert!(
737            !released.load(Ordering::SeqCst),
738            "release should NOT run on acquire error"
739        );
740    }
741
742    #[test]
743    fn ensuring_runs_on_success() {
744        let ran = Arc::new(AtomicBool::new(false));
745        let r = ran.clone();
746        let io = IO::succeed(42).ensuring(move || {
747            r.store(true, Ordering::SeqCst);
748        });
749        assert_eq!(io.run().unwrap(), 42);
750        assert!(ran.load(Ordering::SeqCst));
751    }
752
753    #[test]
754    fn ensuring_runs_on_error() {
755        let ran = Arc::new(AtomicBool::new(false));
756        let r = ran.clone();
757        let io: IO<i32> = IO::fail(anyhow::anyhow!("err")).ensuring(move || {
758            r.store(true, Ordering::SeqCst);
759        });
760        assert!(io.run().is_err());
761        assert!(ran.load(Ordering::SeqCst));
762    }
763
764    #[test]
765    fn ensuring_preserves_result() {
766        let io = IO::succeed(42).ensuring(|| {});
767        assert_eq!(io.run().unwrap(), 42);
768
769        let io: IO<i32> = IO::fail(anyhow::anyhow!("err")).ensuring(|| {});
770        assert_eq!(io.run().unwrap_err().to_string(), "err");
771    }
772
773    // ===== Step 5: Async interop =====
774
775    #[tokio::test(flavor = "multi_thread")]
776    async fn from_future_success() {
777        let io = IO::from_future(async { Ok(42) });
778        assert_eq!(io.run().unwrap(), 42);
779    }
780
781    #[tokio::test(flavor = "multi_thread")]
782    async fn from_future_error() {
783        let io: IO<i32> = IO::from_future(async { Err(anyhow::anyhow!("async fail")) });
784        assert_eq!(io.run().unwrap_err().to_string(), "async fail");
785    }
786
787    #[tokio::test(flavor = "multi_thread")]
788    async fn to_future_success() {
789        let io = IO::succeed(42);
790        let result = io.to_future().await;
791        assert_eq!(result.unwrap(), 42);
792    }
793
794    #[tokio::test(flavor = "multi_thread")]
795    async fn to_future_error() {
796        let io: IO<i32> = IO::fail(anyhow::anyhow!("err"));
797        let result = io.to_future().await;
798        assert_eq!(result.unwrap_err().to_string(), "err");
799    }
800
801    #[tokio::test(flavor = "multi_thread")]
802    async fn future_roundtrip() {
803        let io = IO::succeed(42);
804        let future = io.to_future();
805        let io2 = IO::from_future(future);
806        assert_eq!(io2.run().unwrap(), 42);
807    }
808
809    // ===== Step 6: Pure trait + fdo! verification =====
810
811    #[test]
812    fn pure_creates_succeed() {
813        let io: IO<i32> = Pure::pure(42);
814        assert_eq!(io.run().unwrap(), 42);
815    }
816
817    #[test]
818    fn fdo_single_bind() {
819        use functype_core::fdo;
820
821        let result = fdo! {
822            x <- IO::succeed(42);
823            yield x
824        };
825        assert_eq!(result.run().unwrap(), 42);
826    }
827
828    #[test]
829    fn fdo_multi_bind() {
830        use functype_core::fdo;
831
832        let result = fdo! {
833            x <- IO::succeed(10);
834            y <- IO::succeed(20);
835            yield x + y
836        };
837        assert_eq!(result.run().unwrap(), 30);
838    }
839
840    #[test]
841    fn fdo_short_circuits_on_fail() {
842        use functype_core::fdo;
843
844        let ran = Arc::new(AtomicBool::new(false));
845        let r = ran.clone();
846        let result: IO<i32> = {
847            let r2 = r;
848            fdo! {
849                _x <- IO::<i32>::fail(anyhow::anyhow!("fail"));
850                y <- IO::effect(move || { r2.store(true, Ordering::SeqCst); 99 });
851                yield y
852            }
853        };
854        assert!(result.run().is_err());
855        assert!(!ran.load(Ordering::SeqCst));
856    }
857
858    #[test]
859    fn fdo_let_binding() {
860        use functype_core::fdo;
861
862        let result = fdo! {
863            x <- IO::succeed(10);
864            let doubled = x * 2;
865            y <- IO::succeed(3);
866            yield doubled + y
867        };
868        assert_eq!(result.run().unwrap(), 23);
869    }
870
871    #[test]
872    fn fdo_nested() {
873        use functype_core::fdo;
874
875        let result = fdo! {
876            x <- IO::succeed(10);
877            y <- fdo! {
878                a <- IO::succeed(20);
879                yield a + 1
880            };
881            yield x + y
882        };
883        assert_eq!(result.run().unwrap(), 31);
884    }
885
886    #[test]
887    fn fdo_realistic_pipeline() {
888        use functype_core::fdo;
889
890        fn parse_int(s: &str) -> IO<i32> {
891            match s.parse::<i32>() {
892                Ok(n) => IO::succeed(n),
893                Err(e) => IO::fail(anyhow::anyhow!("parse error: {}", e)),
894            }
895        }
896
897        fn safe_div(a: i32, b: i32) -> IO<i32> {
898            if b == 0 {
899                IO::fail(anyhow::anyhow!("division by zero"))
900            } else {
901                IO::succeed(a / b)
902            }
903        }
904
905        let result = fdo! {
906            x <- parse_int("100");
907            y <- parse_int("5");
908            z <- safe_div(x, y);
909            yield z + 1
910        };
911        assert_eq!(result.run().unwrap(), 21);
912
913        let result = fdo! {
914            x <- parse_int("100");
915            y <- parse_int("0");
916            z <- safe_div(x, y);
917            yield z + 1
918        };
919        assert_eq!(result.run().unwrap_err().to_string(), "division by zero");
920    }
921
922    // ===== Step 8: Debug + Task alias =====
923
924    #[test]
925    fn debug_impl() {
926        let io = IO::succeed(42);
927        let debug_str = format!("{:?}", io);
928        assert!(debug_str.contains("IO"));
929    }
930
931    #[test]
932    fn task_alias() {
933        let task: Task<i32> = IO::succeed(42);
934        assert_eq!(task.run().unwrap(), 42);
935    }
936}