Skip to main content

karpal_profunctor/
forget.rs

1// Copyright (C) 2026 Industrial Algebra
2// SPDX-License-Identifier: Apache-2.0
3
4use alloc::boxed::Box;
5use alloc::vec::Vec;
6use core::marker::PhantomData;
7
8use karpal_core::Monoid;
9
10use crate::choice::Choice;
11use crate::profunctor::{HKT2, Profunctor};
12use crate::strong::Strong;
13use crate::traversing::Traversing;
14
15/// Marker type whose `P<A, B> = Box<dyn Fn(A) -> R>`.
16///
17/// `B` is phantom — the second argument to `dimap` is ignored.
18/// This profunctor "forgets" the output and extracts a summary value.
19pub struct ForgetF<R>(PhantomData<R>);
20
21impl<R: 'static> HKT2 for ForgetF<R> {
22    type P<A, B> = Box<dyn Fn(A) -> R>;
23}
24
25impl<R: 'static> Profunctor for ForgetF<R> {
26    fn dimap<A: 'static, B: 'static, C, D>(
27        f: impl Fn(C) -> A + 'static,
28        _g: impl Fn(B) -> D + 'static,
29        pab: Box<dyn Fn(A) -> R>,
30    ) -> Box<dyn Fn(C) -> R> {
31        Box::new(move |c| pab(f(c)))
32    }
33}
34
35impl<R: 'static> Strong for ForgetF<R> {
36    fn first<A, B, C>(pab: Box<dyn Fn(A) -> R>) -> Box<dyn Fn((A, C)) -> R>
37    where
38        A: 'static,
39        B: 'static,
40        C: 'static,
41    {
42        Box::new(move |(a, _)| pab(a))
43    }
44
45    fn second<A, B, C>(pab: Box<dyn Fn(A) -> R>) -> Box<dyn Fn((C, A)) -> R>
46    where
47        A: 'static,
48        B: 'static,
49        C: 'static,
50    {
51        Box::new(move |(_, a)| pab(a))
52    }
53}
54
55impl<R: Monoid + 'static> Choice for ForgetF<R> {
56    fn left<A, B, C>(pab: Box<dyn Fn(A) -> R>) -> Box<dyn Fn(Result<A, C>) -> R>
57    where
58        A: 'static,
59        B: 'static,
60        C: 'static,
61    {
62        Box::new(move |r| match r {
63            Ok(a) => pab(a),
64            Err(_) => R::empty(),
65        })
66    }
67
68    fn right<A, B, C>(pab: Box<dyn Fn(A) -> R>) -> Box<dyn Fn(Result<C, A>) -> R>
69    where
70        A: 'static,
71        B: 'static,
72        C: 'static,
73    {
74        Box::new(move |r| match r {
75            Ok(_) => R::empty(),
76            Err(a) => pab(a),
77        })
78    }
79}
80
81impl<R: Monoid + 'static> Traversing for ForgetF<R> {
82    fn wander<S, T, A, B>(
83        get_all: impl Fn(&S) -> Vec<A> + 'static,
84        _modify_all: impl Fn(S, &dyn Fn(A) -> B) -> T + 'static,
85        pab: Self::P<A, B>,
86    ) -> Self::P<S, T>
87    where
88        S: 'static,
89        T: 'static,
90        A: 'static,
91        B: 'static,
92    {
93        Box::new(move |s: S| {
94            get_all(&s)
95                .into_iter()
96                .map(&*pab)
97                .fold(R::empty(), |acc, r| acc.combine(r))
98        })
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use proptest::prelude::*;
106
107    #[test]
108    fn forget_dimap_ignores_g() {
109        let extract: Box<dyn Fn(i32) -> String> = Box::new(|x| x.to_string());
110        // g should be completely ignored
111        let result = ForgetF::dimap(|x: i32| x + 1, |_: String| 999i32, extract);
112        assert_eq!(result(4), "5"); // f(4) = 5, then extract(5) = "5"
113    }
114
115    #[test]
116    fn forget_strong_first() {
117        let extract: Box<dyn Fn(i32) -> i32> = Box::new(|x| x * 10);
118        let f = <ForgetF<i32> as Strong>::first::<i32, i32, &str>(extract);
119        assert_eq!(f((3, "hi")), 30);
120    }
121
122    #[test]
123    fn forget_strong_second() {
124        let extract: Box<dyn Fn(i32) -> i32> = Box::new(|x| x * 10);
125        let f = <ForgetF<i32> as Strong>::second::<i32, i32, &str>(extract);
126        assert_eq!(f(("hi", 3)), 30);
127    }
128
129    #[test]
130    fn forget_choice_left_match() {
131        let extract: Box<dyn Fn(i32) -> i32> = Box::new(|x| x * 10);
132        let f = <ForgetF<i32> as Choice>::left::<i32, i32, &str>(extract);
133        assert_eq!(f(Ok(3)), 30);
134    }
135
136    #[test]
137    fn forget_choice_left_miss() {
138        let extract: Box<dyn Fn(i32) -> i32> = Box::new(|x| x * 10);
139        let f = <ForgetF<i32> as Choice>::left::<i32, i32, &str>(extract);
140        assert_eq!(f(Err("nope")), 0); // Monoid::empty for i32
141    }
142
143    #[test]
144    fn forget_choice_right_match() {
145        let extract: Box<dyn Fn(i32) -> i32> = Box::new(|x| x * 10);
146        let f = <ForgetF<i32> as Choice>::right::<i32, i32, &str>(extract);
147        assert_eq!(f(Err(3)), 30);
148    }
149
150    #[test]
151    fn forget_phantom_b_verification() {
152        // ForgetF<R>::P<A, B> = Box<dyn Fn(A) -> R>, B is completely phantom
153        let extract: Box<dyn Fn(i32) -> String> = Box::new(|x| format!("got {x}"));
154        // B can be anything — it's never used
155        let result = <ForgetF<String> as Profunctor>::dimap(
156            |x: i32| x,
157            |_: String| vec![1, 2, 3], // g produces Vec<i32>, totally ignored
158            extract,
159        );
160        assert_eq!(result(42), "got 42");
161    }
162
163    proptest! {
164        #[test]
165        fn forget_profunctor_identity(x in any::<i32>()) {
166            let id_fn: Box<dyn Fn(i32) -> i32> = Box::new(|a| a);
167            let dimapped = <ForgetF<i32> as Profunctor>::dimap(|a: i32| a, |b: i32| b, id_fn);
168            prop_assert_eq!(dimapped(x), x);
169        }
170    }
171}