algebra_core/fix.rs
1//! # Fixed-point types and recursion schemes
2//!
3//! This module provides the building blocks for recursion schemes in
4//! Rust:
5//!
6//! - [`TypeApp`]: Higher-kinded type encoding
7//! - [`Functor`]: Functor on one type parameter
8//! - [`Fix`]: Least fixed point (μF)
9//! - [`fold`]: Catamorphism / F-algebra eliminator
10//!
11//! # Example: Linked List
12//!
13//! ```rust
14//! use algebra_core::fix::{TypeApp, Functor, Fix, fold};
15//!
16//! // Base functor for List<A>: ListF<A, X> = Nil | Cons(A, X)
17//! enum ListF<A, X> {
18//! Nil,
19//! Cons(A, X),
20//! }
21//!
22//! // Type-level tag for ListF<A, _>
23//! struct ListTag<A>(std::marker::PhantomData<A>);
24//!
25//! impl<A> TypeApp for ListTag<A> {
26//! type Applied<X> = ListF<A, X>;
27//! }
28//!
29//! impl<A: Clone> Functor for ListTag<A> {
30//! fn fmap<X, Y, G>(fx: ListF<A, X>, mut g: G) -> ListF<A, Y>
31//! where
32//! G: FnMut(X) -> Y,
33//! {
34//! match fx {
35//! ListF::Nil => ListF::Nil,
36//! ListF::Cons(a, x) => ListF::Cons(a.clone(), g(x)),
37//! }
38//! }
39//! }
40//!
41//! // List<A> is the fixed point of ListF<A, _>
42//! type List<A> = Fix<ListTag<A>>;
43//!
44//! // Smart constructors
45//! fn nil<A: Clone>() -> List<A> {
46//! Fix::new(ListF::Nil)
47//! }
48//!
49//! fn cons<A: Clone>(head: A, tail: List<A>) -> List<A> {
50//! Fix::new(ListF::Cons(head, tail))
51//! }
52//!
53//! // Build a list: [1, 2, 3]
54//! let list = cons(1, cons(2, cons(3, nil())));
55//!
56//! // Compute length via fold
57//! let length: usize = fold(list, |layer| match layer {
58//! ListF::Nil => 0,
59//! ListF::Cons(_, n) => n + 1,
60//! });
61//! assert_eq!(length, 3);
62//! ```
63
64use std::fmt::Debug;
65
66/// A **type constructor** encoding via associated types.
67///
68/// Rust lacks higher-kinded types, so we encode `F : Type → Type` as:
69/// - A marker type `F` (the "tag")
70/// - An associated type `Applied<X>` representing `F(X)`
71///
72/// # Example
73///
74/// ```rust
75/// use algebra_core::fix::TypeApp;
76///
77/// // Option as a type constructor
78/// struct OptionTag;
79///
80/// impl TypeApp for OptionTag {
81/// type Applied<X> = Option<X>;
82/// }
83///
84/// // Now OptionTag::Applied<i32> == Option<i32>
85/// let x: <OptionTag as TypeApp>::Applied<i32> = Some(42);
86/// ```
87pub trait TypeApp {
88 /// The result of applying this type constructor to `X`.
89 type Applied<X>;
90}
91
92/// A **functor** for type constructors.
93///
94/// The `: TypeApp` bound is a kind signature — it says `Functor` is for
95/// type constructors (`* -> *`), not a behavioral dependency.
96///
97/// This is a functor in the category-theoretic sense: it maps objects
98/// (types) and morphisms (functions) while preserving identity and
99/// composition.
100///
101/// Laws (not enforced by type system):
102///
103/// - **Identity**: `fmap id = id`
104/// - **Composition**: `fmap g ∘ fmap f = fmap (g ∘ f)`
105///
106/// # Example
107///
108/// ```rust
109/// use algebra_core::fix::{TypeApp, Functor};
110///
111/// // A simple container
112/// struct Pair<X>(X, X);
113///
114/// struct PairTag;
115///
116/// impl TypeApp for PairTag {
117/// type Applied<X> = Pair<X>;
118/// }
119///
120/// impl Functor for PairTag {
121/// fn fmap<X, Y, G>(fx: Pair<X>, mut g: G) -> Pair<Y>
122/// where
123/// G: FnMut(X) -> Y,
124/// {
125/// Pair(g(fx.0), g(fx.1))
126/// }
127/// }
128/// ```
129pub trait Functor: TypeApp {
130 /// Map a function over the holes (type parameter positions).
131 fn fmap<X, Y, G>(fx: Self::Applied<X>, g: G) -> Self::Applied<Y>
132 where
133 G: FnMut(X) -> Y;
134}
135
136/// The **least fixed point** (μF) of a functor F.
137///
138/// `Fix<F>` satisfies the isomorphism:
139///
140/// ```text
141/// Fix<F> ≅ F(Fix<F>)
142/// ```
143///
144/// # Why `Fix<F>` satisfies `X ≅ F(X)` (for a Haskeller)
145///
146/// We freely mix real Haskell syntax with informal *type-equation
147/// expansions* to show what the types *mean*. Some lines below are
148/// explanatory and are not meant to compile literally.
149///
150/// Start with an ordinary recursive datatype:
151///
152/// ```haskell
153/// data List a = Nil | Cons a (List a)
154/// ```
155///
156/// Read this as a sum/product equation over values:
157///
158/// ```text
159/// List a ≅ 1 + (a × List a)
160/// ```
161///
162/// where:
163///
164/// - `1` is the singleton set (the `Nil` case),
165/// - `+` is disjoint sum (choice of constructor),
166/// - `×` is product (constructor arguments).
167///
168/// At this point we are no longer doing category theory — we are
169/// solving a *type equation*. A recursive datatype is precisely one
170/// whose values are defined in terms of smaller values of the same
171/// type. Writing the constructors this way makes that explicit: we
172/// are looking for a type `X` such that `X` appears on both sides of
173/// its own definition. That is exactly what a fixed point is.
174///
175/// Now factor out the recursive position by naming the shape that
176/// still has a hole.
177///
178/// In Haskell syntax:
179///
180/// ```haskell
181/// data ListF a x = Nil | Cons a x
182/// ```
183///
184/// Schematic (equational) form of the same declaration:
185///
186/// ```text
187/// ListF a X = 1 + (a × X)
188/// ```
189///
190/// This separates the *shape* of the structure from the recursion
191/// itself.
192///
193/// Substituting the original type back into the hole gives:
194///
195/// ```text
196/// List a ≅ ListF a (List a)
197/// ```
198///
199/// which is exactly the fixed-point equation:
200///
201/// ```text
202/// X ≅ F(X)
203/// ```
204///
205/// with `X = List a` and `F = ListF a`.
206///
207/// We write `≅` (isomorphism), not `=`, because recursive types are
208/// represented by wrappers, not by definitional equality. In Haskell
209/// this is explicit:
210///
211/// ```haskell
212/// newtype Fix f = Fix (f (Fix f))
213///
214/// unFix :: Fix f -> f (Fix f)
215/// ```
216///
217/// In Rust, direct self-recursion is not representable without
218/// indirection, so `Fix<F>` stores `F::Applied<Fix<F>>` inside a
219/// `Box`. This establishes the same isomorphism by construction
220/// rather than by definitional equality.
221///
222/// Unrolling makes the correspondence concrete by showing what "one
223/// layer" means.
224///
225/// Specialize `Fix` to lists:
226///
227/// ```haskell
228/// type List a = Fix (ListF a)
229/// ```
230///
231/// Now compute `unFix` at that specialization:
232///
233/// ```haskell
234/// unFix :: List a -> ListF a (List a)
235/// :: Fix (ListF a) -> ListF a (Fix (ListF a))
236///
237/// -- meta-level expansion of the constructors (not valid Haskell syntax)
238/// :: Fix (ListF a) -> (Nil | Cons a (Fix (ListF a)))
239/// ```
240///
241/// That final line is not Haskell code — it is the semantic expansion
242/// of the datatype. It shows that one step of unrolling a list yields
243/// exactly one constructor layer: either `Nil`, or `Cons a` paired
244/// with a recursive tail.
245///
246/// In Rust, the same unrolling exists with different names and
247/// explicit indirection:
248///
249/// ```text
250/// Fix<F>::out : Fix<F> -> F::Applied<Fix<F>>
251/// ```
252///
253/// For `F = ListTag<A>` where `F::Applied<X> = ListF<A, X>`:
254///
255/// ```text
256/// Fix<ListTag<A>>::out : Fix<ListTag<A>> -> ListF<A, Fix<ListTag<A>>>
257/// ```
258///
259/// The `Box` is purely representational — it makes the recursive type
260/// finite in memory — and does not change the mathematical meaning of
261/// the unrolling.
262///
263/// # Why "least" fixed point?
264///
265/// The equation `X = F(X)` can have multiple solutions, including
266/// infinite, non-well-founded ones. The least fixed point `μF` is the
267/// smallest solution closed under the constructors — the inductive
268/// (finite) values.
269///
270/// In domain-theoretic terms, `μF` is the join of an ascending chain:
271///
272/// ```text
273/// μF = ⊔{Fⁿ(⊥) | n ∈ ℕ}
274/// ```
275///
276/// For a concrete intuition, let:
277///
278/// ```text
279/// ExprF<X> = Lit(i32) | Add(X, X)
280/// ```
281///
282/// Then:
283///
284/// - `E₀ = ⊥` (empty)
285/// - `E₁ = Lit(n)`
286/// - `E₂ = Lit(n) | Add(Lit, Lit)`
287/// - `E₃ = Lit(n) | Add(E₂, E₂)`
288/// - ...
289/// - `μExprF = ⊔Eₙ` = all finite expression trees
290///
291/// Each stage adds one more layer of depth. The fixed point is the
292/// union of all finite stages — exactly the recursive datatype we
293/// intend to model.
294///
295/// # `Send` / `Sync`
296///
297/// `Fix<F>` is `Send` iff `F::Applied<Fix<F>>` is `Send`.
298/// `Fix<F>` is `Sync` iff `F::Applied<Fix<F>>` is `Sync`.
299///
300/// # Example
301///
302/// ```rust
303/// use algebra_core::fix::{TypeApp, Fix};
304///
305/// // Natural numbers: Nat = Zero | Succ(Nat)
306/// enum NatF<X> {
307/// Zero,
308/// Succ(X),
309/// }
310///
311/// struct NatTag;
312///
313/// impl TypeApp for NatTag {
314/// type Applied<X> = NatF<X>;
315/// }
316///
317/// type Nat = Fix<NatTag>;
318///
319/// // Smart constructors hide the Fix::new calls
320/// fn zero() -> Nat { Fix::new(NatF::Zero) }
321/// fn succ(n: Nat) -> Nat { Fix::new(NatF::Succ(n)) }
322///
323/// let two = succ(succ(zero()));
324/// ```
325#[repr(transparent)]
326pub struct Fix<F: TypeApp>(Box<F::Applied<Fix<F>>>);
327
328impl<F: TypeApp> Fix<F> {
329 /// Construct a `Fix` from one layer of the functor.
330 ///
331 /// This is the "in" morphism: `F(Fix F) → Fix F`
332 #[inline]
333 pub fn new(node: F::Applied<Fix<F>>) -> Self {
334 Fix(Box::new(node))
335 }
336
337 /// Unwrap one layer of the functor, consuming the `Fix`.
338 ///
339 /// This is the "out" morphism: `Fix F → F(Fix F)`
340 #[inline]
341 pub fn out(self) -> F::Applied<Fix<F>> {
342 *self.0
343 }
344
345 /// Borrow one layer of the functor.
346 #[inline]
347 pub fn as_out(&self) -> &F::Applied<Fix<F>> {
348 &self.0
349 }
350}
351
352impl<F> Clone for Fix<F>
353where
354 F: TypeApp,
355 F::Applied<Fix<F>>: Clone,
356{
357 fn clone(&self) -> Self {
358 Self(Box::new((*self.0).clone()))
359 }
360}
361
362impl<F> Debug for Fix<F>
363where
364 F: TypeApp,
365 F::Applied<Fix<F>>: Debug,
366{
367 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
368 f.debug_tuple("Fix").field(&*self.0).finish()
369 }
370}
371
372impl<F> PartialEq for Fix<F>
373where
374 F: TypeApp,
375 F::Applied<Fix<F>>: PartialEq,
376{
377 fn eq(&self, other: &Self) -> bool {
378 *self.0 == *other.0
379 }
380}
381
382impl<F> Eq for Fix<F>
383where
384 F: TypeApp,
385 F::Applied<Fix<F>>: Eq,
386{
387}
388
389/// A **catamorphism**: fold a recursive structure using an F-algebra.
390///
391/// Given an algebra `alg : F(A) → A`, this produces a function
392/// `Fix<F> → A` that recursively applies the algebra from the
393/// leaves up.
394///
395/// # Stack Safety
396///
397/// This implementation uses direct recursion. The risk of stack
398/// overflow is proportional to the **recursion depth**, not the total
399/// size of the structure.
400///
401/// - **Balanced trees**: Typically safe (depth ≈ log(n))
402/// - **Skewed structures** (e.g., long linked lists): May overflow
403///
404/// Stack overflow typically occurs around ~10k frames on common
405/// platforms, but this varies. For very deep structures, consider
406/// using the `stacker` crate or an iterative approach.
407///
408/// # Example
409///
410/// ```rust
411/// use algebra_core::fix::{TypeApp, Functor, Fix, fold};
412///
413/// // Expression tree: Expr = Lit(i32) | Add(Expr, Expr)
414/// enum ExprF<X> {
415/// Lit(i32),
416/// Add(X, X),
417/// }
418///
419/// struct ExprTag;
420///
421/// impl TypeApp for ExprTag {
422/// type Applied<X> = ExprF<X>;
423/// }
424///
425/// impl Functor for ExprTag {
426/// fn fmap<X, Y, G>(fx: ExprF<X>, mut g: G) -> ExprF<Y>
427/// where
428/// G: FnMut(X) -> Y,
429/// {
430/// match fx {
431/// ExprF::Lit(n) => ExprF::Lit(n),
432/// ExprF::Add(l, r) => ExprF::Add(g(l), g(r)),
433/// }
434/// }
435/// }
436///
437/// type Expr = Fix<ExprTag>;
438///
439/// fn lit(n: i32) -> Expr {
440/// Fix::new(ExprF::Lit(n))
441/// }
442///
443/// fn add(l: Expr, r: Expr) -> Expr {
444/// Fix::new(ExprF::Add(l, r))
445/// }
446///
447/// // Build: (1 + 2) + 3
448/// let expr = add(add(lit(1), lit(2)), lit(3));
449///
450/// // Evaluate via fold
451/// let result: i32 = fold(expr, |layer| match layer {
452/// ExprF::Lit(n) => n,
453/// ExprF::Add(l, r) => l + r,
454/// });
455/// assert_eq!(result, 6);
456/// ```
457pub fn fold<F, A>(t: Fix<F>, alg: impl FnMut(F::Applied<A>) -> A) -> A
458where
459 F: Functor,
460{
461 // Internal helper avoids moving the closure on each recursive call
462 fn go<F, A>(t: Fix<F>, alg: &mut impl FnMut(F::Applied<A>) -> A) -> A
463 where
464 F: Functor,
465 {
466 let node = t.out();
467 let mapped = F::fmap(node, |child| go::<F, A>(child, alg));
468 alg(mapped)
469 }
470
471 let mut alg = alg;
472 go::<F, A>(t, &mut alg)
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478
479 // Test fixtures: ListF as base functor
480
481 // Note: We don't derive Clone/Debug/PartialEq on ListF because
482 // the derived impls cause trait resolution overflow when used
483 // with Fix<ListTag<A>> (recursive type). Instead, we implement
484 // them manually or test without requiring these traits on Fix.
485 enum ListF<A, X> {
486 Nil,
487 Cons(A, X),
488 }
489
490 // Manual implementations for non-recursive X (used in functor law
491 // tests)
492 impl<A: Clone, X: Clone> Clone for ListF<A, X> {
493 fn clone(&self) -> Self {
494 match self {
495 ListF::Nil => ListF::Nil,
496 ListF::Cons(a, x) => ListF::Cons(a.clone(), x.clone()),
497 }
498 }
499 }
500
501 impl<A: PartialEq, X: PartialEq> PartialEq for ListF<A, X> {
502 fn eq(&self, other: &Self) -> bool {
503 match (self, other) {
504 (ListF::Nil, ListF::Nil) => true,
505 (ListF::Cons(a1, x1), ListF::Cons(a2, x2)) => a1 == a2 && x1 == x2,
506 _ => false,
507 }
508 }
509 }
510
511 impl<A: Eq, X: Eq> Eq for ListF<A, X> {}
512
513 impl<A: Debug, X: Debug> Debug for ListF<A, X> {
514 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
515 match self {
516 ListF::Nil => write!(f, "Nil"),
517 ListF::Cons(a, x) => f.debug_tuple("Cons").field(a).field(x).finish(),
518 }
519 }
520 }
521
522 struct ListTag<A>(std::marker::PhantomData<A>);
523
524 impl<A> TypeApp for ListTag<A> {
525 type Applied<X> = ListF<A, X>;
526 }
527
528 impl<A: Clone> Functor for ListTag<A> {
529 fn fmap<X, Y, G>(fx: ListF<A, X>, mut g: G) -> ListF<A, Y>
530 where
531 G: FnMut(X) -> Y,
532 {
533 match fx {
534 ListF::Nil => ListF::Nil,
535 ListF::Cons(a, x) => ListF::Cons(a.clone(), g(x)),
536 }
537 }
538 }
539
540 type List<A> = Fix<ListTag<A>>;
541
542 fn nil<A: Clone>() -> List<A> {
543 Fix::new(ListF::Nil)
544 }
545
546 fn cons<A: Clone>(head: A, tail: List<A>) -> List<A> {
547 Fix::new(ListF::Cons(head, tail))
548 }
549
550 fn from_vec<A: Clone>(v: Vec<A>) -> List<A> {
551 v.into_iter().rev().fold(nil(), |acc, x| cons(x, acc))
552 }
553
554 // Helper to convert List back to Vec for comparison
555 fn to_vec(list: List<i32>) -> Vec<i32> {
556 fold(list, |layer: ListF<i32, Vec<i32>>| match layer {
557 ListF::Nil => Vec::new(),
558 ListF::Cons(a, mut acc) => {
559 acc.insert(0, a);
560 acc
561 }
562 })
563 }
564
565 // Fix tests
566
567 #[test]
568 fn fix_new_and_out_are_inverses() {
569 // Test with a non-recursive inner type to avoid overflow
570 let layer: ListF<i32, String> = ListF::Cons(42, "tail".to_string());
571 let fixed: Fix<ListTag<i32>> = Fix(Box::new(ListF::Cons(42, nil())));
572
573 // Verify we can unwrap and get back the expected structure
574 match fixed.out() {
575 ListF::Cons(head, _tail) => assert_eq!(head, 42),
576 ListF::Nil => panic!("expected Cons"),
577 }
578
579 // Also test with Nil
580 let nil_fixed: Fix<ListTag<i32>> = Fix::new(ListF::Nil);
581 match nil_fixed.out() {
582 ListF::Nil => {}
583 ListF::Cons(_, _) => panic!("expected Nil"),
584 }
585
586 // Verify layer clone works for non-recursive case
587 let cloned = layer.clone();
588 assert_eq!(cloned, layer);
589 }
590
591 #[test]
592 fn fix_as_out_borrows_layer() {
593 let list = cons(1, cons(2, nil()));
594 match list.as_out() {
595 ListF::Cons(head, _) => assert_eq!(*head, 1),
596 ListF::Nil => panic!("expected Cons"),
597 }
598 }
599
600 #[test]
601 fn fix_operations_preserve_structure() {
602 // Build a list and verify via fold that structure is
603 // preserved
604 let list = cons(1, cons(2, cons(3, nil())));
605 let vec = to_vec(list);
606 assert_eq!(vec, vec![1, 2, 3]);
607 }
608
609 // fold tests
610
611 #[test]
612 fn fold_computes_length() {
613 let list = from_vec(vec![1, 2, 3, 4, 5]);
614 let length: usize = fold(list, |layer: ListF<i32, usize>| match layer {
615 ListF::Nil => 0,
616 ListF::Cons(_, n) => n + 1,
617 });
618 assert_eq!(length, 5);
619 }
620
621 #[test]
622 fn fold_computes_sum() {
623 let list = from_vec(vec![1, 2, 3, 4, 5]);
624 let sum: i32 = fold(list, |layer: ListF<i32, i32>| match layer {
625 ListF::Nil => 0,
626 ListF::Cons(a, acc) => a + acc,
627 });
628 assert_eq!(sum, 15);
629 }
630
631 #[test]
632 fn fold_converts_to_vec() {
633 let list = from_vec(vec![1, 2, 3]);
634 let vec = to_vec(list);
635 assert_eq!(vec, vec![1, 2, 3]);
636 }
637
638 #[test]
639 fn fold_empty_list() {
640 let list: List<i32> = nil();
641 let length: usize = fold(list, |layer: ListF<i32, usize>| match layer {
642 ListF::Nil => 0,
643 ListF::Cons(_, n) => n + 1,
644 });
645 assert_eq!(length, 0);
646 }
647
648 // Functor law tests (using non-recursive X to enable
649 // Clone/PartialEq)
650
651 #[test]
652 fn functor_identity_law() {
653 // fmap(fx, |x| x) == fx
654 let layer: ListF<i32, String> = ListF::Cons(42, "tail".to_string());
655 let mapped = ListTag::<i32>::fmap(layer.clone(), |x| x);
656 assert_eq!(mapped, layer);
657 }
658
659 #[test]
660 fn functor_identity_law_nil() {
661 let layer: ListF<i32, String> = ListF::Nil;
662 let mapped = ListTag::<i32>::fmap(layer.clone(), |x| x);
663 assert_eq!(mapped, layer);
664 }
665
666 #[test]
667 fn functor_composition_law() {
668 // fmap(fmap(fx, f), g) == fmap(fx, |x| g(f(x)))
669 let layer: ListF<i32, i32> = ListF::Cons(1, 10);
670
671 let f = |x: i32| x * 2;
672 let g = |x: i32| x + 1;
673
674 // fmap twice
675 let left = ListTag::<i32>::fmap(ListTag::<i32>::fmap(layer.clone(), f), g);
676
677 // fmap once with composition
678 let right = ListTag::<i32>::fmap(layer, |x| g(f(x)));
679
680 assert_eq!(left, right);
681 }
682
683 #[test]
684 fn functor_composition_law_nil() {
685 let layer: ListF<i32, i32> = ListF::Nil;
686
687 let f = |x: i32| x * 2;
688 let g = |x: i32| x + 1;
689
690 let left = ListTag::<i32>::fmap(ListTag::<i32>::fmap(layer.clone(), f), g);
691 let right = ListTag::<i32>::fmap(layer, |x| g(f(x)));
692
693 assert_eq!(left, right);
694 }
695
696 // Expression tree example (from docs)
697
698 enum ExprF<X> {
699 Lit(i32),
700 Add(X, X),
701 }
702
703 struct ExprTag;
704
705 impl TypeApp for ExprTag {
706 type Applied<X> = ExprF<X>;
707 }
708
709 impl Functor for ExprTag {
710 fn fmap<X, Y, G>(fx: ExprF<X>, mut g: G) -> ExprF<Y>
711 where
712 G: FnMut(X) -> Y,
713 {
714 match fx {
715 ExprF::Lit(n) => ExprF::Lit(n),
716 ExprF::Add(l, r) => ExprF::Add(g(l), g(r)),
717 }
718 }
719 }
720
721 type Expr = Fix<ExprTag>;
722
723 fn lit(n: i32) -> Expr {
724 Fix::new(ExprF::Lit(n))
725 }
726
727 fn add(l: Expr, r: Expr) -> Expr {
728 Fix::new(ExprF::Add(l, r))
729 }
730
731 #[test]
732 fn expr_eval_via_fold() {
733 // (1 + 2) + 3 = 6
734 let expr = add(add(lit(1), lit(2)), lit(3));
735 let result: i32 = fold(expr, |layer: ExprF<i32>| match layer {
736 ExprF::Lit(n) => n,
737 ExprF::Add(l, r) => l + r,
738 });
739 assert_eq!(result, 6);
740 }
741
742 #[test]
743 fn expr_count_nodes_via_fold() {
744 // (1 + 2) + 3 has 5 nodes: 3 Lit, 2 Add
745 let expr = add(add(lit(1), lit(2)), lit(3));
746 let count: usize = fold(expr, |layer: ExprF<usize>| match layer {
747 ExprF::Lit(_) => 1,
748 ExprF::Add(l, r) => 1 + l + r,
749 });
750 assert_eq!(count, 5);
751 }
752
753 #[test]
754 fn expr_to_string_via_fold() {
755 let expr = add(add(lit(1), lit(2)), lit(3));
756 let s: String = fold(expr, |layer: ExprF<String>| match layer {
757 ExprF::Lit(n) => n.to_string(),
758 ExprF::Add(l, r) => format!("({} + {})", l, r),
759 });
760 assert_eq!(s, "((1 + 2) + 3)");
761 }
762}