1use std::fmt;
2
3use anyhow;
4use functype_core::either::Either;
5use functype_core::pure::Pure;
6
7pub 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
44pub type Task<A> = IO<A>;
46
47impl<A: Send + 'static> IO<A> {
52 pub fn succeed(a: A) -> IO<A> {
54 IO {
55 thunk: Box::new(move || Ok(a)),
56 }
57 }
58
59 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 pub fn effect(f: impl FnOnce() -> A + Send + 'static) -> IO<A> {
68 IO {
69 thunk: Box::new(move || Ok(f())),
70 }
71 }
72
73 pub fn effect_result(f: impl FnOnce() -> Result<A, anyhow::Error> + Send + 'static) -> IO<A> {
75 IO { thunk: Box::new(f) }
76 }
77
78 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 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 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
103impl<A> IO<A> {
108 pub fn run(self) -> Result<A, anyhow::Error> {
110 (self.thunk)()
111 }
112
113 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
122impl<A: Send + 'static> IO<A> {
127 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 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 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 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 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 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 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
197impl<A: Send + 'static> IO<A> {
202 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 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 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 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 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
262impl IO<()> {
267 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 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
303impl<A: Send + 'static> IO<A> {
308 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 pub async fn to_future(self) -> Result<A, anyhow::Error> {
323 self.run()
324 }
325}
326
327impl<A: Send + 'static> Pure<A> for IO<A> {
332 fn pure(a: A) -> Self {
333 IO::succeed(a)
334 }
335}
336
337#[cfg(test)]
342mod tests {
343 use super::*;
344 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
345 use std::sync::Arc;
346
347 #[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 #[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 #[test]
560 fn monad_left_identity() {
561 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 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 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 #[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); }
677
678 #[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); 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 #[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 #[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 #[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}