Skip to main content

fp_library/types/
coyoneda_explicit.rs

1//! Coyoneda with the intermediate type made explicit, enabling zero-cost map fusion.
2//!
3//! [`CoyonedaExplicit`] is the same construction as [`Coyoneda`](crate::types::Coyoneda)
4//! but without existential quantification over the intermediate type `B`. Where `Coyoneda`
5//! hides `B` behind a trait object (enabling HKT integration), `CoyonedaExplicit` exposes
6//! `B` as a type parameter (enabling compile-time function composition). The corresponding brand is [`CoyonedaExplicitBrand`](crate::brands::CoyonedaExplicitBrand).
7//!
8//! ## Map fusion
9//!
10//! Each call to [`map`](CoyonedaExplicit::map) composes the new function with the
11//! accumulated function at compile time. No boxing, no dynamic dispatch, no heap
12//! allocation. At [`lower`](CoyonedaExplicit::lower) time, a single call to `F::map`
13//! applies the fully composed function regardless of how many maps were chained.
14//! Use [`.boxed()`](CoyonedaExplicit::boxed) when a uniform type is needed (struct
15//! fields, loops, collections).
16//!
17//! For chains deeper than ~20-30 maps, consider inserting `.boxed()` to bound
18//! compile-time type complexity.
19//!
20//! ## Send / Sync
21//!
22//! The compiler derives `Send` automatically when `Func: Send` and
23//! `F::Of<'a, B>: Send`. No separate `SendCoyonedaExplicit` type is needed. Use
24//! [`.boxed_send()`](CoyonedaExplicit::boxed_send) to erase the function type while
25//! preserving `Send`.
26//!
27//! ## Trade-offs vs `Coyoneda`
28//!
29//! | Property | `Coyoneda` | `CoyonedaExplicit` |
30//! | -------- | ---------- | ------------------ |
31//! | HKT integration | Yes (has a brand, implements `Functor`) | No |
32//! | Map fusion | No (k calls to `F::map`) | Yes (1 call to `F::map`) |
33//! | Heap allocation per map | 1 box (function stored inline) | 0 (1 box with `.boxed()`) |
34//! | Stack overflow risk | Yes (deep nesting) | No (compiler inlines; use `.boxed()` for deep chains) |
35//! | Foldable without Functor | No | Yes |
36//! | Hoist without Functor | No | Yes |
37//! | Pointed via brand | Yes | No |
38//! | Semimonad via brand | Yes | No |
39//! | `B: 'static` required for brand | No | Yes |
40//!
41//! ## When to use which
42//!
43//! Use `Coyoneda` when you need HKT polymorphism (e.g., writing code generic over any
44//! `Functor`). Use `CoyonedaExplicit` when you need zero-cost map fusion on a known
45//! type constructor, or when composing many maps in a performance-sensitive path.
46//!
47//! ### Examples
48//!
49//! ```
50//! use fp_library::{
51//! 	brands::*,
52//! 	functions::*,
53//! 	types::*,
54//! };
55//!
56//! let result = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3])
57//! 	.map(|x| x + 1)
58//! 	.map(|x| x * 2)
59//! 	.map(|x| x.to_string())
60//! 	.lower();
61//!
62//! // Only one call to Vec::map, applying the composed function x -> (x + 1) * 2 -> string.
63//! assert_eq!(result, vec!["4", "6", "8"]);
64//! ```
65
66#[fp_macros::document_module]
67mod inner {
68	use {
69		crate::{
70			brands::CoyonedaExplicitBrand,
71			classes::*,
72			functions::{
73				compose,
74				identity,
75			},
76			impl_kind,
77			kinds::*,
78			types::Coyoneda,
79		},
80		fp_macros::*,
81		std::marker::PhantomData,
82	};
83
84	/// Coyoneda with an explicit intermediate type, enabling zero-cost map fusion.
85	///
86	/// Stores a value of type `F B` alongside a function `B -> A`. Each call to
87	/// [`map`](CoyonedaExplicit::map) composes the new function with the existing one
88	/// at the type level, producing a new `CoyonedaExplicit` with an updated function
89	/// type but the same underlying `F B`. At [`lower`](CoyonedaExplicit::lower) time,
90	/// a single `F::map` applies the fully composed function.
91	///
92	/// No boxing, no dynamic dispatch, no heap allocation occurs during `map`. Use
93	/// [`.boxed()`](CoyonedaExplicit::boxed) as an escape hatch when a uniform type is
94	/// needed (struct fields, loops, collections).
95	///
96	/// Unlike [`Coyoneda`](crate::types::Coyoneda), the intermediate type `B` is visible
97	/// as a type parameter rather than hidden behind a trait object. This prevents HKT
98	/// integration (no brand or `Functor` instance) but reduces lowering to a single
99	/// `F::map` call with zero overhead per `map`.
100	#[document_type_parameters(
101		"The lifetime of the values.",
102		"The brand of the underlying type constructor.",
103		"The type of the values in the underlying functor (the input to the accumulated function).",
104		"The current output type (the output of the accumulated function).",
105		"The type of the accumulated function from `B` to `A`."
106	)]
107	pub struct CoyonedaExplicit<
108		'a,
109		F,
110		B: 'a,
111		A: 'a,
112		Func: Fn(B) -> A + 'a = Box<dyn Fn(B) -> A + 'a>,
113	>
114	where
115		F: Kind_cdc7cd43dac7585f + 'a, {
116		fb: Apply!(<F as Kind!( type Of<'a, T: 'a>: 'a; )>::Of<'a, B>),
117		func: Func,
118		_phantom: PhantomData<A>,
119	}
120
121	/// Type alias for a [`CoyonedaExplicit`] with a boxed function, for use in
122	/// struct fields, collections, loops, and HKT brands.
123	pub type BoxedCoyonedaExplicit<'a, F, B, A> =
124		CoyonedaExplicit<'a, F, B, A, Box<dyn Fn(B) -> A + 'a>>;
125
126	#[document_type_parameters(
127		"The lifetime of the values.",
128		"The brand of the underlying type constructor.",
129		"The type of the values in the underlying functor.",
130		"The current output type.",
131		"The type of the accumulated function."
132	)]
133	#[document_parameters("The `CoyonedaExplicit` instance.")]
134	impl<'a, F, B: 'a, A: 'a, Func: Fn(B) -> A + 'a> CoyonedaExplicit<'a, F, B, A, Func>
135	where
136		F: Kind_cdc7cd43dac7585f + 'a,
137	{
138		/// Construct a `CoyonedaExplicit` from a function and a functor value.
139		///
140		/// Stores `fb` alongside `f` as a single deferred mapping step.
141		/// [`lift`](CoyonedaExplicit::lift) is equivalent to `new(|a| a, fa)`.
142		#[document_signature]
143		///
144		#[document_parameters("The function to defer.", "The functor value.")]
145		///
146		#[document_returns("A `CoyonedaExplicit` wrapping the value with the deferred function.")]
147		#[document_examples]
148		///
149		/// ```
150		/// use fp_library::{
151		/// 	brands::*,
152		/// 	types::*,
153		/// };
154		///
155		/// let coyo = CoyonedaExplicit::<VecBrand, _, _, _>::new(|x: i32| x * 2, vec![1, 2, 3]);
156		/// assert_eq!(coyo.lower(), vec![2, 4, 6]);
157		/// ```
158		pub fn new(
159			f: Func,
160			fb: Apply!(<F as Kind!( type Of<'a, T: 'a>: 'a; )>::Of<'a, B>),
161		) -> Self {
162			CoyonedaExplicit {
163				fb,
164				func: f,
165				_phantom: PhantomData,
166			}
167		}
168
169		/// Map a function over the value, composing it with the accumulated function.
170		///
171		/// This composes `f` with the stored function. No heap allocation occurs;
172		/// the composition is stored inline.
173		/// At [`lower`](CoyonedaExplicit::lower) time, a single `F::map` call applies
174		/// the fully composed function.
175		#[document_signature]
176		///
177		#[document_type_parameters("The new output type after applying the function.")]
178		///
179		#[document_parameters("The function to compose.")]
180		///
181		#[document_returns("A new `CoyonedaExplicit` with the composed function.")]
182		#[document_examples]
183		///
184		/// ```
185		/// use fp_library::{
186		/// 	brands::*,
187		/// 	types::*,
188		/// };
189		///
190		/// let result = CoyonedaExplicit::<OptionBrand, _, _, _>::lift(Some(5))
191		/// 	.map(|x| x * 2)
192		/// 	.map(|x| x + 1)
193		/// 	.lower();
194		///
195		/// assert_eq!(result, Some(11));
196		/// ```
197		pub fn map<C: 'a>(
198			self,
199			f: impl Fn(A) -> C + 'a,
200		) -> CoyonedaExplicit<'a, F, B, C, impl Fn(B) -> C + 'a> {
201			CoyonedaExplicit {
202				fb: self.fb,
203				func: compose(f, self.func),
204				_phantom: PhantomData,
205			}
206		}
207
208		/// Lower the `CoyonedaExplicit` back to the underlying functor `F`.
209		///
210		/// Applies the accumulated composed function in a single call to `F::map`.
211		/// Requires `F: Functor`.
212		#[document_signature]
213		///
214		#[document_returns("The underlying functor value with the composed function applied.")]
215		#[document_examples]
216		///
217		/// ```
218		/// use fp_library::{
219		/// 	brands::*,
220		/// 	types::*,
221		/// };
222		///
223		/// let result = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3])
224		/// 	.map(|x| x + 1)
225		/// 	.map(|x| x * 2)
226		/// 	.lower();
227		///
228		/// assert_eq!(result, vec![4, 6, 8]);
229		/// ```
230		pub fn lower(self) -> Apply!(<F as Kind!( type Of<'a, T: 'a>: 'a; )>::Of<'a, A>)
231		where
232			F: Functor, {
233			F::map(self.func, self.fb)
234		}
235
236		/// Apply a natural transformation to the underlying functor.
237		///
238		/// Transforms a `CoyonedaExplicit<F, B, A>` into a `CoyonedaExplicit<G, B, A>`
239		/// by applying the natural transformation directly to the stored `F B`. Unlike
240		/// [`Coyoneda::hoist`](crate::types::Coyoneda::hoist), this does not require
241		/// `F: Functor` because the intermediate type `B` is visible.
242		#[document_signature]
243		///
244		#[document_type_parameters("The brand of the target functor.")]
245		///
246		#[document_parameters("The natural transformation from `F` to `G`.")]
247		///
248		#[document_returns("A new `CoyonedaExplicit` over the target functor `G`.")]
249		#[document_examples]
250		///
251		/// ```
252		/// use fp_library::{
253		/// 	brands::*,
254		/// 	classes::*,
255		/// 	types::*,
256		/// };
257		///
258		/// struct VecToOption;
259		/// impl NaturalTransformation<VecBrand, OptionBrand> for VecToOption {
260		/// 	fn transform<'a, A: 'a>(
261		/// 		&self,
262		/// 		fa: Vec<A>,
263		/// 	) -> Option<A> {
264		/// 		fa.into_iter().next()
265		/// 	}
266		/// }
267		///
268		/// let coyo = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![10, 20, 30]).map(|x| x * 2);
269		/// let hoisted = coyo.hoist(VecToOption);
270		/// assert_eq!(hoisted.lower(), Some(20));
271		/// ```
272		pub fn hoist<G: Kind_cdc7cd43dac7585f + 'a>(
273			self,
274			nat: impl NaturalTransformation<F, G>,
275		) -> CoyonedaExplicit<'a, G, B, A, Func> {
276			CoyonedaExplicit {
277				fb: nat.transform(self.fb),
278				func: self.func,
279				_phantom: PhantomData,
280			}
281		}
282
283		/// Fold the structure by composing the fold function with the accumulated
284		/// mapping function, then folding the original `F B` in a single pass.
285		///
286		/// Unlike [`Foldable for CoyonedaBrand`](crate::classes::Foldable), this does
287		/// not require `F: Functor`. It only requires `F: Foldable`, matching
288		/// PureScript's semantics. No intermediate `F A` is materialized.
289		#[document_signature]
290		///
291		#[document_type_parameters(
292			"The brand of the cloneable function to use.",
293			"The monoid type to fold into."
294		)]
295		///
296		#[document_parameters("The function mapping each element to a monoid value.")]
297		///
298		#[document_returns("The combined monoid value.")]
299		#[document_examples]
300		///
301		/// ```
302		/// use fp_library::{
303		/// 	brands::*,
304		/// 	types::*,
305		/// };
306		///
307		/// let result = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3])
308		/// 	.map(|x| x * 10)
309		/// 	.fold_map::<RcFnBrand, _>(|x: i32| x.to_string());
310		///
311		/// assert_eq!(result, "102030".to_string());
312		/// ```
313		pub fn fold_map<FnBrand, M>(
314			self,
315			func: impl Fn(A) -> M + 'a,
316		) -> M
317		where
318			B: Clone,
319			M: Monoid + 'a,
320			F: Foldable,
321			FnBrand: LiftFn + 'a, {
322			F::fold_map::<FnBrand, B, M>(compose(func, self.func), self.fb)
323		}
324
325		/// Fold the structure with index by composing the fold function with the
326		/// accumulated mapping function, then folding the original `F B` in a
327		/// single pass.
328		///
329		/// This does not require `F: Functor`, only `F: FoldableWithIndex`,
330		/// matching PureScript's semantics. No intermediate `F A` is materialized.
331		/// The index comes from `F`'s `FoldableWithIndex` instance.
332		#[document_signature]
333		///
334		#[document_type_parameters(
335			"The brand of the cloneable function to use.",
336			"The monoid type to fold into."
337		)]
338		///
339		#[document_parameters("The function mapping each index and element to a monoid value.")]
340		///
341		#[document_returns("The combined monoid value.")]
342		#[document_examples]
343		///
344		/// ```
345		/// use fp_library::{
346		/// 	brands::*,
347		/// 	types::*,
348		/// };
349		///
350		/// let result = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3])
351		/// 	.map(|x| x * 10)
352		/// 	.fold_map_with_index::<RcFnBrand, _>(|i: usize, x: i32| format!("{i}:{x}"));
353		///
354		/// assert_eq!(result, "0:101:202:30".to_string());
355		/// ```
356		pub fn fold_map_with_index<FnBrand, M>(
357			self,
358			func: impl Fn(<F as WithIndex>::Index, A) -> M + 'a,
359		) -> M
360		where
361			B: Clone,
362			M: Monoid + 'a,
363			F: FoldableWithIndex,
364			FnBrand: LiftFn + 'a,
365			<F as WithIndex>::Index: 'a, {
366			let f = self.func;
367			F::fold_map_with_index::<FnBrand, _, _>(move |i, b| func(i, f(b)), self.fb)
368		}
369
370		/// Traverse the structure by composing the traversal function with the
371		/// accumulated mapping function, traversing the original `F B` in a
372		/// single pass, and wrapping the result in `CoyonedaExplicit`.
373		///
374		/// This is a fusion barrier: all accumulated maps are composed into the
375		/// traversal function and applied during the traversal. The resulting
376		/// `CoyonedaExplicit` starts fresh with the identity function.
377		///
378		/// This does not require `F: Functor` beyond what `F: Traversable`
379		/// already implies. Matches PureScript's `Traversable (Coyoneda f)`
380		/// semantics.
381		#[document_signature]
382		///
383		#[document_type_parameters(
384			"The applicative context brand.",
385			"The output element type after traversal."
386		)]
387		///
388		#[document_parameters(
389			"The function mapping each element to a value in the applicative context."
390		)]
391		///
392		#[document_returns(
393			"The traversed result wrapped in the applicative context, containing a `CoyonedaExplicit` in identity position."
394		)]
395		#[document_examples]
396		///
397		/// ```
398		/// use fp_library::{
399		/// 	brands::*,
400		/// 	types::*,
401		/// };
402		///
403		/// let coyo = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3]).map(|x| x * 10);
404		/// let result: Option<CoyonedaExplicit<VecBrand, _, _, _>> =
405		/// 	coyo.traverse::<OptionBrand, _>(|x| if x > 0 { Some(x) } else { None });
406		/// assert_eq!(result.map(|c| c.lower()), Some(vec![10, 20, 30]));
407		/// ```
408		#[allow(
409			clippy::type_complexity,
410			reason = "HKT return type with nested Apply! is inherently complex"
411		)]
412		pub fn traverse<G: Applicative + 'a, C: 'a + Clone>(
413			self,
414			f: impl Fn(A) -> Apply!(<G as Kind!( type Of<'a, T: 'a>: 'a; )>::Of<'a, C>) + 'a,
415		) -> Apply!(<G as Kind!( type Of<'a, T: 'a>: 'a; )>::Of<'a, CoyonedaExplicit<'a, F, C, C, fn(C) -> C>>)
416		where
417			B: Clone,
418			F: Traversable,
419			Apply!(<F as Kind!( type Of<'a, T: 'a>: 'a; )>::Of<'a, C>): Clone,
420			Apply!(<G as Kind!( type Of<'a, T: 'a>: 'a; )>::Of<'a, C>): Clone, {
421			G::map(
422				|fc| CoyonedaExplicit::lift(fc),
423				F::traverse::<B, C, G>(compose(f, self.func), self.fb),
424			)
425		}
426
427		/// Apply a wrapped function to this value by lowering both sides, delegating to
428		/// `F::apply`, and re-lifting the result.
429		///
430		/// This is the `Semiapplicative` operation lifted to `CoyonedaExplicit` without
431		/// requiring a brand. After the operation the fusion pipeline is reset: the
432		/// result is a `CoyonedaExplicit` with the identity function and intermediate
433		/// type `C`.
434		///
435		/// This is a fusion barrier: it calls `lower()` on both arguments,
436		/// materializing all accumulated maps before delegating to `F::apply`.
437		#[document_signature]
438		///
439		#[document_type_parameters(
440			"The brand of the cloneable function wrapper.",
441			"The intermediate type of the function container.",
442			"The output type after applying the function.",
443			"The type of the function in the function container."
444		)]
445		///
446		#[document_parameters(
447			"The `CoyonedaExplicit` containing the wrapped function.",
448			"The `CoyonedaExplicit` containing the value."
449		)]
450		///
451		#[document_returns(
452			"A `CoyonedaExplicit` containing the result of applying the function to the value."
453		)]
454		#[document_examples]
455		///
456		/// ```
457		/// use fp_library::{
458		/// 	brands::*,
459		/// 	classes::*,
460		/// 	functions::*,
461		/// 	types::*,
462		/// };
463		///
464		/// let ff = CoyonedaExplicit::<OptionBrand, _, _, _>::lift(Some(lift_fn_new::<RcFnBrand, _, _>(
465		/// 	|x: i32| x * 2,
466		/// )));
467		/// let fa = CoyonedaExplicit::<OptionBrand, _, _, _>::lift(Some(5i32));
468		/// let result = CoyonedaExplicit::apply::<RcFnBrand, _, _, _>(ff, fa).lower();
469		/// assert_eq!(result, Some(10));
470		/// ```
471		pub fn apply<
472			FnBrand: LiftFn + 'a,
473			Bf: 'a,
474			C: 'a,
475			FuncF: Fn(Bf) -> <FnBrand as CloneFn>::Of<'a, A, C> + 'a,
476		>(
477			ff: CoyonedaExplicit<'a, F, Bf, <FnBrand as CloneFn>::Of<'a, A, C>, FuncF>,
478			fa: Self,
479		) -> CoyonedaExplicit<'a, F, C, C, fn(C) -> C>
480		where
481			A: Clone,
482			F: Semiapplicative, {
483			CoyonedaExplicit::lift(F::apply::<FnBrand, A, C>(ff.lower(), fa.lower()))
484		}
485
486		/// Bind through the accumulated function directly, composing the callback
487		/// with the accumulated mapping function and delegating to `F::bind`.
488		///
489		/// This is a fusion barrier: all accumulated maps are composed into the
490		/// bind callback and applied during the bind. The resulting
491		/// `CoyonedaExplicit` starts fresh with the identity function.
492		///
493		/// The callback `f` receives the mapped value (after the accumulated
494		/// function is applied) and returns a raw `F::Of<'a, C>` directly. This
495		/// avoids needing `F: Functor` and skips an intermediate `F::map` traversal.
496		/// After the operation the fusion pipeline is reset: the result is a
497		/// `CoyonedaExplicit` with the identity function and intermediate type `C`.
498		#[document_signature]
499		///
500		#[document_type_parameters("The output type of the bound computation.")]
501		///
502		#[document_parameters(
503			"The function to apply to each mapped value, returning a raw functor value."
504		)]
505		///
506		#[document_returns("A `CoyonedaExplicit` containing the bound result.")]
507		#[document_examples]
508		///
509		/// ```
510		/// use fp_library::{
511		/// 	brands::*,
512		/// 	types::*,
513		/// };
514		///
515		/// let fa = CoyonedaExplicit::<OptionBrand, _, _, _>::lift(Some(5i32)).map(|x| x * 2);
516		/// let result = fa.bind(|x| Some(x + 1)).lower();
517		/// assert_eq!(result, Some(11)); // (5 * 2) + 1
518		/// ```
519		pub fn bind<C: 'a>(
520			self,
521			f: impl Fn(A) -> Apply!(<F as Kind!( type Of<'a, T: 'a>: 'a; )>::Of<'a, C>) + 'a,
522		) -> CoyonedaExplicit<'a, F, C, C, fn(C) -> C>
523		where
524			F: Semimonad, {
525			let func = self.func;
526			CoyonedaExplicit::lift(F::bind(self.fb, move |b| f(func(b))))
527		}
528
529		/// Erase the function type by boxing it.
530		///
531		/// This is the escape hatch for storing in struct fields, collections, or
532		/// loop accumulators where a uniform type is needed. Reintroduces one
533		/// `Box` allocation and dynamic dispatch.
534		///
535		/// When used in a loop (e.g., `coyo = coyo.map(f).boxed()` per iteration),
536		/// each iteration creates a closure that captures the previous boxed function.
537		/// The composed function chain has O(k) per-element overhead at `lower` time,
538		/// matching `Coyoneda`'s cost profile. The single-`F::map`-call advantage of
539		/// `CoyonedaExplicit` is fully realized only with static (compile-time)
540		/// composition where the compiler can inline the function chain.
541		#[document_signature]
542		///
543		#[document_returns("A `BoxedCoyonedaExplicit` with the function boxed.")]
544		#[document_examples]
545		///
546		/// ```
547		/// use fp_library::{
548		/// 	brands::*,
549		/// 	types::*,
550		/// };
551		///
552		/// let coyo = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3]).map(|x| x + 1);
553		/// let boxed: BoxedCoyonedaExplicit<VecBrand, i32, i32> = coyo.boxed();
554		/// assert_eq!(boxed.lower(), vec![2, 3, 4]);
555		/// ```
556		pub fn boxed(self) -> BoxedCoyonedaExplicit<'a, F, B, A> {
557			CoyonedaExplicit {
558				fb: self.fb,
559				func: Box::new(self.func),
560				_phantom: PhantomData,
561			}
562		}
563
564		/// Erase the function type by boxing it with `Send`.
565		///
566		/// Like [`boxed`](CoyonedaExplicit::boxed), but the resulting function is
567		/// `Send`, allowing the value to cross thread boundaries.
568		#[document_signature]
569		///
570		#[document_returns("A `CoyonedaExplicit` with the function boxed as `Send`.")]
571		#[document_examples]
572		///
573		/// ```
574		/// use fp_library::{
575		/// 	brands::*,
576		/// 	types::*,
577		/// };
578		///
579		/// let coyo = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3]).map(|x| x + 1);
580		/// let boxed = coyo.boxed_send();
581		/// assert_eq!(boxed.lower(), vec![2, 3, 4]);
582		/// ```
583		pub fn boxed_send(self) -> CoyonedaExplicit<'a, F, B, A, Box<dyn Fn(B) -> A + Send + 'a>>
584		where
585			Func: Send, {
586			CoyonedaExplicit {
587				fb: self.fb,
588				func: Box::new(self.func),
589				_phantom: PhantomData,
590			}
591		}
592	}
593
594	#[document_type_parameters(
595		"The lifetime of the values.",
596		"The brand of the underlying type constructor.",
597		"The type of the values in the functor."
598	)]
599	impl<'a, F, A: 'a> CoyonedaExplicit<'a, F, A, A, fn(A) -> A>
600	where
601		F: Kind_cdc7cd43dac7585f + 'a,
602	{
603		/// Lift a value of `F A` into `CoyonedaExplicit` with the identity function.
604		///
605		/// This is the starting point for building a fusion pipeline. The intermediate
606		/// type `B` and the output type `A` are the same.
607		#[document_signature]
608		///
609		#[document_parameters("The functor value to lift.")]
610		///
611		#[document_returns("A `CoyonedaExplicit` wrapping the value with the identity function.")]
612		#[document_examples]
613		///
614		/// ```
615		/// use fp_library::{
616		/// 	brands::*,
617		/// 	types::*,
618		/// };
619		///
620		/// let coyo = CoyonedaExplicit::<OptionBrand, _, _, _>::lift(Some(42));
621		/// assert_eq!(coyo.lower(), Some(42));
622		/// ```
623		pub fn lift(fa: Apply!(<F as Kind!( type Of<'a, T: 'a>: 'a; )>::Of<'a, A>)) -> Self {
624			CoyonedaExplicit {
625				fb: fa,
626				func: identity as fn(A) -> A,
627				_phantom: PhantomData,
628			}
629		}
630	}
631
632	#[document_type_parameters(
633		"The lifetime of the values.",
634		"The brand of the underlying pointed functor.",
635		"The type of the value."
636	)]
637	impl<'a, F, A: 'a> CoyonedaExplicit<'a, F, A, A, fn(A) -> A>
638	where
639		F: Pointed + 'a,
640	{
641		/// Wrap a pure value in a `CoyonedaExplicit` context.
642		///
643		/// Delegates to `F::pure` and wraps with the identity function.
644		#[document_signature]
645		///
646		#[document_parameters("The value to wrap.")]
647		///
648		#[document_returns("A `CoyonedaExplicit` containing the pure value.")]
649		#[document_examples]
650		///
651		/// ```
652		/// use fp_library::{
653		/// 	brands::*,
654		/// 	types::*,
655		/// };
656		///
657		/// let coyo = CoyonedaExplicit::<OptionBrand, _, _, _>::pure(42);
658		/// assert_eq!(coyo.lower(), Some(42));
659		/// ```
660		pub fn pure(a: A) -> Self {
661			Self::lift(F::pure(a))
662		}
663	}
664
665	#[document_type_parameters(
666		"The lifetime of the values.",
667		"The brand of the underlying type constructor.",
668		"The type of the values in the underlying functor.",
669		"The current output type.",
670		"The type of the accumulated function."
671	)]
672	impl<'a, F, B: 'a, A: 'a, Func: Fn(B) -> A + 'a> From<CoyonedaExplicit<'a, F, B, A, Func>>
673		for Coyoneda<'a, F, A>
674	where
675		F: Kind_cdc7cd43dac7585f + 'a,
676	{
677		/// Convert a [`CoyonedaExplicit`] into a [`Coyoneda`] by hiding the
678		/// intermediate type `B` behind a trait object.
679		///
680		/// This is useful when you have finished building a fusion pipeline and
681		/// need to pass the result into code that is generic over `Functor` via
682		/// `CoyonedaBrand`.
683		///
684		/// Note: further `map` calls on the resulting `Coyoneda` do not fuse with
685		/// the previously composed function; each adds a separate trait-object
686		/// layer.
687		#[document_signature]
688		///
689		#[document_parameters("The `CoyonedaExplicit` to convert.")]
690		///
691		#[document_returns("A `Coyoneda` wrapping the same value with the accumulated function.")]
692		#[document_examples]
693		///
694		/// ```
695		/// use fp_library::{
696		/// 	brands::*,
697		/// 	types::*,
698		/// };
699		///
700		/// let explicit = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3]).map(|x| x + 1);
701		/// let coyo: Coyoneda<VecBrand, i32> = explicit.into();
702		/// assert_eq!(coyo.lower(), vec![2, 3, 4]);
703		/// ```
704		fn from(explicit: CoyonedaExplicit<'a, F, B, A, Func>) -> Self {
705			Coyoneda::new(explicit.func, explicit.fb)
706		}
707	}
708
709	// -- Brand --
710
711	impl_kind! {
712		impl<F: Kind_cdc7cd43dac7585f + 'static, B: 'static> for CoyonedaExplicitBrand<F, B> {
713			type Of<'a, A: 'a>: 'a = BoxedCoyonedaExplicit<'a, F, B, A>;
714		}
715	}
716
717	// -- Functor for CoyonedaExplicitBrand --
718
719	#[document_type_parameters(
720		"The brand of the underlying type constructor.",
721		"The type of the values in the underlying functor."
722	)]
723	impl<F: Kind_cdc7cd43dac7585f + 'static, B: 'static> Functor for CoyonedaExplicitBrand<F, B> {
724		/// Maps a function over the `BoxedCoyonedaExplicit` by composing it with the
725		/// accumulated function, then re-boxing.
726		///
727		/// Does not require `F: Functor`. The function is composed at the type level
728		/// and a single `F::map` call applies the result at
729		/// [`lower`](CoyonedaExplicit::lower) time. This preserves single-pass fusion,
730		/// unlike [`CoyonedaBrand`](crate::brands::CoyonedaBrand) which adds a separate
731		/// layer per map.
732		///
733		/// Note: each call through this brand-level `map` allocates a `Box` for the
734		/// composed function (via `.boxed()`). Zero-cost fusion (no allocation per map)
735		/// is only available via the inherent [`CoyonedaExplicit::map`] method, which
736		/// uses compile-time function composition without boxing.
737		#[document_signature]
738		///
739		#[document_type_parameters(
740			"The lifetime of the values.",
741			"The type of the current output.",
742			"The type of the new output."
743		)]
744		///
745		#[document_parameters("The function to apply.", "The `BoxedCoyonedaExplicit` value.")]
746		///
747		#[document_returns("A new `BoxedCoyonedaExplicit` with the composed function.")]
748		#[document_examples]
749		///
750		/// ```
751		/// use fp_library::{
752		/// 	brands::*,
753		/// 	functions::*,
754		/// 	types::*,
755		/// };
756		///
757		/// let coyo = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3]).boxed();
758		/// let mapped =
759		/// 	explicit::map::<CoyonedaExplicitBrand<VecBrand, i32>, _, _, _, _>(|x| x * 10, coyo);
760		/// assert_eq!(mapped.lower(), vec![10, 20, 30]);
761		/// ```
762		fn map<'a, A: 'a, C: 'a>(
763			func: impl Fn(A) -> C + 'a,
764			fa: Apply!(<Self as Kind!( type Of<'a, T: 'a>: 'a; )>::Of<'a, A>),
765		) -> Apply!(<Self as Kind!( type Of<'a, T: 'a>: 'a; )>::Of<'a, C>) {
766			fa.map(func).boxed()
767		}
768	}
769
770	// -- Foldable for CoyonedaExplicitBrand --
771
772	#[document_type_parameters(
773		"The brand of the underlying foldable type constructor.",
774		"The type of the values in the underlying functor."
775	)]
776	impl<F: Kind_cdc7cd43dac7585f + Foldable + 'static, B: Clone + 'static> Foldable
777		for CoyonedaExplicitBrand<F, B>
778	{
779		/// Folds the `BoxedCoyonedaExplicit` by composing the fold function with the
780		/// accumulated mapping function, then folding the original `F B` in a single
781		/// pass.
782		///
783		/// Unlike [`Foldable for CoyonedaBrand`](crate::classes::Foldable), this does
784		/// not require `F: Functor`. It only requires `F: Foldable`, matching
785		/// PureScript's semantics. No intermediate `F A` is materialized.
786		#[document_signature]
787		///
788		#[document_type_parameters(
789			"The lifetime of the elements.",
790			"The brand of the cloneable function to use.",
791			"The type of the elements in the structure.",
792			"The type of the monoid."
793		)]
794		///
795		#[document_parameters(
796			"The function to map each element to a monoid.",
797			"The `BoxedCoyonedaExplicit` structure to fold."
798		)]
799		///
800		#[document_returns("The combined monoid value.")]
801		#[document_examples]
802		///
803		/// ```
804		/// use fp_library::{
805		/// 	brands::*,
806		/// 	functions::*,
807		/// 	types::*,
808		/// };
809		///
810		/// let coyo = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3]).map(|x| x * 10).boxed();
811		///
812		/// let result = explicit::fold_map::<RcFnBrand, CoyonedaExplicitBrand<VecBrand, i32>, _, _, _, _>(
813		/// 	|x: i32| x.to_string(),
814		/// 	coyo,
815		/// );
816		/// assert_eq!(result, "102030".to_string());
817		/// ```
818		fn fold_map<'a, FnBrand, A: 'a + Clone, M>(
819			func: impl Fn(A) -> M + 'a,
820			fa: Apply!(<Self as Kind!( type Of<'a, T: 'a>: 'a; )>::Of<'a, A>),
821		) -> M
822		where
823			M: Monoid + 'a,
824			FnBrand: LiftFn + 'a, {
825			fa.fold_map::<FnBrand, M>(func)
826		}
827	}
828
829	// -- Debug --
830
831	#[document_type_parameters(
832		"The lifetime of the values.",
833		"The brand of the underlying type constructor.",
834		"The type of the values in the underlying functor.",
835		"The current output type.",
836		"The type of the accumulated function."
837	)]
838	#[document_parameters("The `CoyonedaExplicit` instance.")]
839	impl<'a, F, B: 'a, A: 'a, Func> core::fmt::Debug for CoyonedaExplicit<'a, F, B, A, Func>
840	where
841		F: Kind_cdc7cd43dac7585f + 'a,
842		Func: Fn(B) -> A + 'a,
843	{
844		/// Formats the `CoyonedaExplicit` as an opaque value.
845		///
846		/// The stored function cannot be inspected, so the output
847		/// is always `CoyonedaExplicit(<opaque>)`.
848		#[document_signature]
849		///
850		#[document_parameters("The formatter.")]
851		///
852		#[document_returns("The formatting result.")]
853		#[document_examples]
854		///
855		/// ```
856		/// use fp_library::{
857		/// 	brands::*,
858		/// 	types::*,
859		/// };
860		///
861		/// let coyo = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3]);
862		/// assert_eq!(format!("{:?}", coyo), "CoyonedaExplicit(<opaque>)");
863		/// ```
864		fn fmt(
865			&self,
866			f: &mut core::fmt::Formatter<'_>,
867		) -> core::fmt::Result {
868			f.write_str("CoyonedaExplicit(<opaque>)")
869		}
870	}
871}
872
873pub use inner::*;
874
875#[cfg(test)]
876mod tests {
877	use crate::{
878		brands::*,
879		classes::*,
880		functions::*,
881		types::*,
882	};
883
884	#[test]
885	fn lift_lower_identity_option() {
886		let coyo = CoyonedaExplicit::<OptionBrand, _, _, _>::lift(Some(42));
887		assert_eq!(coyo.lower(), Some(42));
888	}
889
890	#[test]
891	fn lift_lower_identity_none() {
892		let coyo = CoyonedaExplicit::<OptionBrand, i32, i32, _>::lift(None);
893		assert_eq!(coyo.lower(), None);
894	}
895
896	#[test]
897	fn lift_lower_identity_vec() {
898		let coyo = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3]);
899		assert_eq!(coyo.lower(), vec![1, 2, 3]);
900	}
901
902	#[test]
903	fn new_constructor() {
904		let coyo = CoyonedaExplicit::<VecBrand, _, _, _>::new(|x: i32| x * 2, vec![1, 2, 3]);
905		assert_eq!(coyo.lower(), vec![2, 4, 6]);
906	}
907
908	#[test]
909	fn new_is_equivalent_to_lift_then_map() {
910		let f = |x: i32| x.to_string();
911		let v = vec![1, 2, 3];
912
913		let via_new = CoyonedaExplicit::<VecBrand, _, _, _>::new(f, v.clone()).lower();
914		let via_lift_map = CoyonedaExplicit::<VecBrand, _, _, _>::lift(v).map(f).lower();
915
916		assert_eq!(via_new, via_lift_map);
917	}
918
919	#[test]
920	fn single_map_option() {
921		let result = CoyonedaExplicit::<OptionBrand, _, _, _>::lift(Some(5)).map(|x| x * 2).lower();
922		assert_eq!(result, Some(10));
923	}
924
925	#[test]
926	fn chained_maps_vec() {
927		let result = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3])
928			.map(|x| x + 1)
929			.map(|x| x * 2)
930			.map(|x| x.to_string())
931			.lower();
932		assert_eq!(result, vec!["4", "6", "8"]);
933	}
934
935	#[test]
936	fn functor_identity_law() {
937		let result =
938			CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3]).map(identity).lower();
939		assert_eq!(result, vec![1, 2, 3]);
940	}
941
942	#[test]
943	fn functor_composition_law() {
944		let f = |x: i32| x + 1;
945		let g = |x: i32| x * 2;
946
947		let left =
948			CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3]).map(compose(f, g)).lower();
949
950		let right =
951			CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3]).map(g).map(f).lower();
952
953		assert_eq!(left, right);
954	}
955
956	#[test]
957	fn many_chained_maps() {
958		let mut coyo = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![0i64]).boxed();
959		for _ in 0 .. 100 {
960			coyo = coyo.map(|x| x + 1).boxed();
961		}
962		assert_eq!(coyo.lower(), vec![100i64]);
963	}
964
965	#[test]
966	fn map_on_none_stays_none() {
967		let result = CoyonedaExplicit::<OptionBrand, _, _, _>::lift(None::<i32>)
968			.map(|x| x + 1)
969			.map(|x| x * 2)
970			.lower();
971		assert_eq!(result, None);
972	}
973
974	#[test]
975	fn lift_lower_roundtrip_preserves_value() {
976		let original = vec![10, 20, 30];
977		let roundtrip = CoyonedaExplicit::<VecBrand, _, _, _>::lift(original.clone()).lower();
978		assert_eq!(roundtrip, original);
979	}
980
981	// -- Pure tests --
982
983	#[test]
984	fn pure_option() {
985		let coyo = CoyonedaExplicit::<OptionBrand, _, _, _>::pure(42);
986		assert_eq!(coyo.lower(), Some(42));
987	}
988
989	#[test]
990	fn pure_vec() {
991		let coyo = CoyonedaExplicit::<VecBrand, _, _, _>::pure(42);
992		assert_eq!(coyo.lower(), vec![42]);
993	}
994
995	// -- Hoist tests --
996
997	struct VecToOption;
998	impl NaturalTransformation<VecBrand, OptionBrand> for VecToOption {
999		fn transform<'a, A: 'a>(
1000			&self,
1001			fa: Vec<A>,
1002		) -> Option<A> {
1003			fa.into_iter().next()
1004		}
1005	}
1006
1007	#[test]
1008	fn hoist_vec_to_option() {
1009		let coyo = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![10, 20, 30]);
1010		let hoisted = coyo.hoist(VecToOption);
1011		assert_eq!(hoisted.lower(), Some(10));
1012	}
1013
1014	#[test]
1015	fn hoist_preserves_accumulated_maps() {
1016		let coyo = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3]).map(|x| x * 10);
1017		let hoisted = coyo.hoist(VecToOption);
1018		assert_eq!(hoisted.lower(), Some(10));
1019	}
1020
1021	#[test]
1022	fn hoist_then_map() {
1023		let coyo = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![5, 10, 15]);
1024		let hoisted = coyo.hoist(VecToOption).map(|x: i32| x.to_string());
1025		assert_eq!(hoisted.lower(), Some("5".to_string()));
1026	}
1027
1028	// -- Fold tests --
1029
1030	#[test]
1031	fn fold_map_on_lifted_vec() {
1032		let result = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3])
1033			.fold_map::<RcFnBrand, _>(|x: i32| x.to_string());
1034		assert_eq!(result, "123".to_string());
1035	}
1036
1037	#[test]
1038	fn fold_map_on_mapped_vec() {
1039		let result = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3])
1040			.map(|x| x * 10)
1041			.fold_map::<RcFnBrand, _>(|x: i32| x.to_string());
1042		assert_eq!(result, "102030".to_string());
1043	}
1044
1045	#[test]
1046	fn fold_map_on_none_is_empty() {
1047		let result = CoyonedaExplicit::<OptionBrand, i32, i32, _>::lift(None)
1048			.map(|x| x + 1)
1049			.fold_map::<RcFnBrand, _>(|x: i32| x.to_string());
1050		assert_eq!(result, String::new());
1051	}
1052
1053	// -- Traverse tests --
1054
1055	#[test]
1056	fn traverse_vec_to_option_all_pass() {
1057		let coyo = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3]).map(|x| x * 10);
1058		let result: Option<CoyonedaExplicit<VecBrand, _, _, _>> =
1059			coyo.traverse::<OptionBrand, _>(|x| if x > 0 { Some(x) } else { None });
1060		assert_eq!(result.map(|c| c.lower()), Some(vec![10, 20, 30]));
1061	}
1062
1063	#[test]
1064	fn traverse_vec_to_option_one_fails() {
1065		let coyo = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, -2, 3]).map(|x| x * 10);
1066		let result: Option<CoyonedaExplicit<VecBrand, _, _, _>> =
1067			coyo.traverse::<OptionBrand, _>(|x| if x > 0 { Some(x) } else { None });
1068		assert_eq!(result.map(|c| c.lower()), None);
1069	}
1070
1071	#[test]
1072	fn traverse_option_to_vec() {
1073		let coyo = CoyonedaExplicit::<OptionBrand, _, _, _>::lift(Some(5)).map(|x| x * 2);
1074		let result: Vec<CoyonedaExplicit<OptionBrand, _, _, _>> =
1075			coyo.traverse::<VecBrand, _>(|x| vec![x, x + 1]);
1076		let lowered: Vec<Option<i32>> = result.into_iter().map(|c| c.lower()).collect();
1077		assert_eq!(lowered, vec![Some(10), Some(11)]);
1078	}
1079
1080	#[test]
1081	fn traverse_lifted_identity() {
1082		let coyo = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3]);
1083		let result: Option<CoyonedaExplicit<VecBrand, _, _, _>> =
1084			coyo.traverse::<OptionBrand, _>(Some);
1085		assert_eq!(result.map(|c| c.lower()), Some(vec![1, 2, 3]));
1086	}
1087
1088	// -- FoldableWithIndex tests --
1089
1090	#[test]
1091	fn fold_map_with_index_on_vec() {
1092		let result = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3])
1093			.map(|x| x * 10)
1094			.fold_map_with_index::<RcFnBrand, _>(|i: usize, x: i32| format!("{i}:{x}"));
1095		assert_eq!(result, "0:101:202:30".to_string());
1096	}
1097
1098	#[test]
1099	fn fold_map_with_index_on_lifted_vec() {
1100		let result = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![10, 20, 30])
1101			.fold_map_with_index::<RcFnBrand, _>(|i: usize, x: i32| format!("{i}:{x}"));
1102		assert_eq!(result, "0:101:202:30".to_string());
1103	}
1104
1105	#[test]
1106	fn fold_map_with_index_on_option() {
1107		let result = CoyonedaExplicit::<OptionBrand, _, _, _>::lift(Some(42))
1108			.map(|x| x + 1)
1109			.fold_map_with_index::<RcFnBrand, _>(|_: (), x: i32| x.to_string());
1110		assert_eq!(result, "43".to_string());
1111	}
1112
1113	#[test]
1114	fn fold_map_with_index_on_none() {
1115		let result = CoyonedaExplicit::<OptionBrand, i32, i32, _>::lift(None)
1116			.map(|x| x + 1)
1117			.fold_map_with_index::<RcFnBrand, _>(|_: (), x: i32| x.to_string());
1118		assert_eq!(result, String::new());
1119	}
1120
1121	// -- Conversion tests --
1122
1123	#[test]
1124	fn into_coyoneda_preserves_semantics() {
1125		let explicit = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3])
1126			.map(|x| x + 1)
1127			.map(|x| x * 2);
1128		let coyo: Coyoneda<VecBrand, i32> = explicit.into();
1129		assert_eq!(coyo.lower(), vec![4, 6, 8]);
1130	}
1131
1132	#[test]
1133	fn into_coyoneda_from_lift() {
1134		let explicit = CoyonedaExplicit::<OptionBrand, _, _, _>::lift(Some(42));
1135		let coyo: Coyoneda<OptionBrand, i32> = explicit.into();
1136		assert_eq!(coyo.lower(), Some(42));
1137	}
1138
1139	// -- Apply tests --
1140
1141	#[test]
1142	fn apply_some_to_some() {
1143		let ff = CoyonedaExplicit::<OptionBrand, _, _, _>::lift(Some(
1144			lift_fn_new::<RcFnBrand, _, _>(|x: i32| x * 2),
1145		));
1146		let fa = CoyonedaExplicit::<OptionBrand, _, _, _>::lift(Some(5i32));
1147		let result = CoyonedaExplicit::apply::<RcFnBrand, _, _, _>(ff, fa).lower();
1148		assert_eq!(result, Some(10));
1149	}
1150
1151	#[test]
1152	fn apply_none_fn_to_some() {
1153		let ff = CoyonedaExplicit::<OptionBrand, _, _, _>::lift(
1154			None::<<RcFnBrand as CloneFn>::Of<'_, i32, i32>>,
1155		);
1156		let fa = CoyonedaExplicit::<OptionBrand, _, _, _>::lift(Some(5i32));
1157		let result = CoyonedaExplicit::apply::<RcFnBrand, _, _, _>(ff, fa).lower();
1158		assert_eq!(result, None);
1159	}
1160
1161	#[test]
1162	fn apply_some_fn_to_none() {
1163		let ff = CoyonedaExplicit::<OptionBrand, _, _, _>::lift(Some(
1164			lift_fn_new::<RcFnBrand, _, _>(|x: i32| x * 2),
1165		));
1166		let fa = CoyonedaExplicit::<OptionBrand, i32, i32, _>::lift(None);
1167		let result = CoyonedaExplicit::apply::<RcFnBrand, _, _, _>(ff, fa).lower();
1168		assert_eq!(result, None);
1169	}
1170
1171	#[test]
1172	fn apply_vec_applies_each_fn_to_each_value() {
1173		let ff = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![
1174			lift_fn_new::<RcFnBrand, _, _>(|x: i32| x + 1),
1175			lift_fn_new::<RcFnBrand, _, _>(|x: i32| x * 10),
1176		]);
1177		let fa = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![2i32, 3]);
1178		let result = CoyonedaExplicit::apply::<RcFnBrand, _, _, _>(ff, fa).lower();
1179		assert_eq!(result, vec![3, 4, 20, 30]);
1180	}
1181
1182	#[test]
1183	fn apply_preserves_prior_maps_on_fa() {
1184		let ff = CoyonedaExplicit::<OptionBrand, _, _, _>::lift(Some(
1185			lift_fn_new::<RcFnBrand, _, _>(|x: i32| x + 1),
1186		));
1187		// Prior map on fa is composed and applied before apply delegates to F.
1188		let fa = CoyonedaExplicit::<OptionBrand, _, _, _>::lift(Some(5i32)).map(|x| x * 2);
1189		let result = CoyonedaExplicit::apply::<RcFnBrand, _, _, _>(ff, fa).lower();
1190		assert_eq!(result, Some(11)); // (5 * 2) + 1
1191	}
1192
1193	// -- Bind tests --
1194
1195	#[test]
1196	fn bind_some() {
1197		let fa = CoyonedaExplicit::<OptionBrand, _, _, _>::lift(Some(5i32));
1198		let result = fa.bind(|x| Some(x * 2)).lower();
1199		assert_eq!(result, Some(10));
1200	}
1201
1202	#[test]
1203	fn bind_none_stays_none() {
1204		let fa = CoyonedaExplicit::<OptionBrand, i32, i32, _>::lift(None);
1205		let result = fa.bind(|x| Some(x * 2)).lower();
1206		assert_eq!(result, None);
1207	}
1208
1209	#[test]
1210	fn bind_returning_none_gives_none() {
1211		let fa = CoyonedaExplicit::<OptionBrand, _, _, _>::lift(Some(5i32));
1212		let result = fa.bind(|_| None::<i32>).lower();
1213		assert_eq!(result, None);
1214	}
1215
1216	#[test]
1217	fn bind_vec() {
1218		let fa = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1i32, 2, 3]);
1219		let result = fa.bind(|x| vec![x, x * 10]).lower();
1220		assert_eq!(result, vec![1, 10, 2, 20, 3, 30]);
1221	}
1222
1223	#[test]
1224	fn bind_uses_accumulated_maps() {
1225		let fa = CoyonedaExplicit::<OptionBrand, _, _, _>::lift(Some(3i32)).map(|x| x * 2);
1226		let result = fa.bind(|x| Some(x + 1)).lower();
1227		assert_eq!(result, Some(7)); // (3 * 2) + 1
1228	}
1229
1230	#[test]
1231	fn bind_vec_with_maps() {
1232		let fa = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1i32, 2, 3]).map(|x| x * 2);
1233		let result = fa.bind(|x| vec![x, x + 1]).lower();
1234		assert_eq!(result, vec![2, 3, 4, 5, 6, 7]);
1235	}
1236
1237	// -- From conversion tests --
1238
1239	#[test]
1240	fn from_explicit_to_coyoneda() {
1241		let explicit = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3])
1242			.map(|x| x + 1)
1243			.map(|x| x * 2);
1244		let coyo: Coyoneda<VecBrand, i32> = explicit.into();
1245		assert_eq!(coyo.lower(), vec![4, 6, 8]);
1246	}
1247
1248	#[test]
1249	fn from_explicit_lift_only() {
1250		let explicit = CoyonedaExplicit::<OptionBrand, _, _, _>::lift(Some(42));
1251		let coyo: Coyoneda<OptionBrand, i32> = explicit.into();
1252		assert_eq!(coyo.lower(), Some(42));
1253	}
1254
1255	// -- Boxed tests --
1256
1257	#[test]
1258	fn test_boxed_erases_type() {
1259		fn assert_same_type<T>(
1260			_a: &T,
1261			_b: &T,
1262		) {
1263		}
1264		let a = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3]).map(|x| x + 1).boxed();
1265		let b = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![4, 5, 6]).map(|x| x * 2).boxed();
1266		assert_same_type(&a, &b);
1267		assert_eq!(a.lower(), vec![2, 3, 4]);
1268		assert_eq!(b.lower(), vec![8, 10, 12]);
1269	}
1270
1271	#[test]
1272	fn test_boxed_send() {
1273		fn assert_send<T: Send>(_: &T) {}
1274		let coyo =
1275			CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3]).map(|x| x + 1).boxed_send();
1276		assert_send(&coyo);
1277		assert_eq!(coyo.lower(), vec![2, 3, 4]);
1278	}
1279
1280	#[test]
1281	fn test_send_auto_derived() {
1282		fn assert_send<T: Send>(_: &T) {}
1283		// fn(i32) -> i32 is Send, Vec<i32> is Send, so the whole thing is Send.
1284		let coyo = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3]);
1285		assert_send(&coyo);
1286	}
1287
1288	// -- Brand tests --
1289
1290	#[test]
1291	fn brand_functor_map() {
1292		let coyo = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3]).boxed();
1293		let mapped =
1294			explicit::map::<CoyonedaExplicitBrand<VecBrand, i32>, _, _, _, _>(|x| x * 10, coyo);
1295		assert_eq!(mapped.lower(), vec![10, 20, 30]);
1296	}
1297
1298	#[test]
1299	fn brand_functor_identity_law() {
1300		let coyo = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3]).boxed();
1301		let result =
1302			explicit::map::<CoyonedaExplicitBrand<VecBrand, i32>, _, _, _, _>(identity, coyo)
1303				.lower();
1304		assert_eq!(result, vec![1, 2, 3]);
1305	}
1306
1307	#[test]
1308	fn brand_functor_composition_law() {
1309		let f = |x: i32| x + 1;
1310		let g = |x: i32| x * 2;
1311
1312		let coyo1 = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3]).boxed();
1313		let left =
1314			explicit::map::<CoyonedaExplicitBrand<VecBrand, i32>, _, _, _, _>(compose(f, g), coyo1)
1315				.lower();
1316
1317		let coyo2 = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3]).boxed();
1318		let right = explicit::map::<CoyonedaExplicitBrand<VecBrand, i32>, _, _, _, _>(
1319			f,
1320			explicit::map::<CoyonedaExplicitBrand<VecBrand, i32>, _, _, _, _>(g, coyo2),
1321		)
1322		.lower();
1323
1324		assert_eq!(left, right);
1325	}
1326
1327	#[test]
1328	fn brand_functor_chained_maps_fuse() {
1329		// Chaining through the brand still produces single-pass fusion.
1330		let coyo = CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3]).boxed();
1331		let result = explicit::map::<CoyonedaExplicitBrand<VecBrand, i32>, _, _, _, _>(
1332			|x: i32| x.to_string(),
1333			explicit::map::<CoyonedaExplicitBrand<VecBrand, i32>, _, _, _, _>(
1334				|x| x * 2,
1335				explicit::map::<CoyonedaExplicitBrand<VecBrand, i32>, _, _, _, _>(|x| x + 1, coyo),
1336			),
1337		)
1338		.lower();
1339		assert_eq!(result, vec!["4", "6", "8"]);
1340	}
1341
1342	#[test]
1343	fn brand_foldable_fold_map() {
1344		let coyo =
1345			CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3]).map(|x| x * 10).boxed();
1346		let result =
1347			explicit::fold_map::<RcFnBrand, CoyonedaExplicitBrand<VecBrand, i32>, _, _, _, _>(
1348				|x: i32| x.to_string(),
1349				coyo,
1350			);
1351		assert_eq!(result, "102030".to_string());
1352	}
1353
1354	#[test]
1355	fn brand_foldable_fold_right() {
1356		let coyo =
1357			CoyonedaExplicit::<VecBrand, _, _, _>::lift(vec![1, 2, 3]).map(|x| x * 2).boxed();
1358		let result =
1359			explicit::fold_right::<RcFnBrand, CoyonedaExplicitBrand<VecBrand, i32>, _, _, _, _>(
1360				|a: i32, b: i32| a + b,
1361				0,
1362				coyo,
1363			);
1364		assert_eq!(result, 12); // (1*2) + (2*2) + (3*2)
1365	}
1366
1367	#[test]
1368	fn brand_foldable_on_none() {
1369		let coyo = CoyonedaExplicit::<OptionBrand, i32, i32, _>::lift(None).map(|x| x + 1).boxed();
1370		let result =
1371			explicit::fold_map::<RcFnBrand, CoyonedaExplicitBrand<OptionBrand, i32>, _, _, _, _>(
1372				|x: i32| x.to_string(),
1373				coyo,
1374			);
1375		assert_eq!(result, String::new());
1376	}
1377
1378	// -- Property-based tests --
1379
1380	mod property {
1381		use {
1382			crate::{
1383				brands::*,
1384				functions::*,
1385				types::*,
1386			},
1387			quickcheck_macros::quickcheck,
1388		};
1389
1390		#[quickcheck]
1391		fn functor_identity_vec(v: Vec<i32>) -> bool {
1392			let coyo = CoyonedaExplicit::<VecBrand, _, _, _>::lift(v.clone()).boxed();
1393			explicit::map::<CoyonedaExplicitBrand<VecBrand, i32>, _, _, _, _>(identity, coyo)
1394				.lower() == v
1395		}
1396
1397		#[quickcheck]
1398		fn functor_identity_option(x: Option<i32>) -> bool {
1399			let coyo = CoyonedaExplicit::<OptionBrand, _, _, _>::lift(x).boxed();
1400			explicit::map::<CoyonedaExplicitBrand<OptionBrand, i32>, _, _, _, _>(identity, coyo)
1401				.lower() == x
1402		}
1403
1404		#[quickcheck]
1405		fn functor_composition_vec(v: Vec<i32>) -> bool {
1406			let f = |x: i32| x.wrapping_add(1);
1407			let g = |x: i32| x.wrapping_mul(2);
1408
1409			let left =
1410				CoyonedaExplicit::<VecBrand, _, _, _>::lift(v.clone()).map(compose(f, g)).lower();
1411			let right = CoyonedaExplicit::<VecBrand, _, _, _>::lift(v).map(g).map(f).lower();
1412
1413			left == right
1414		}
1415	}
1416}