Skip to main content

karpal_arrow/
fn_arrow.rs

1// Copyright (C) 2026 Industrial Algebra
2// SPDX-License-Identifier: Apache-2.0
3
4use karpal_core::hkt::HKT2;
5
6use crate::arrow::Arrow;
7use crate::arrow_apply::ArrowApply;
8use crate::arrow_choice::ArrowChoice;
9use crate::arrow_loop::ArrowLoop;
10use crate::category::Category;
11use crate::semigroupoid::Semigroupoid;
12
13/// Marker type whose `P<A, B>` is `Box<dyn Fn(A) -> B>`.
14///
15/// This is the canonical Arrow instance: the function arrow.
16/// Equivalent to `FnP` in karpal-profunctor but independent (no cross-crate dep).
17pub struct FnA;
18
19impl HKT2 for FnA {
20    type P<A, B> = Box<dyn Fn(A) -> B>;
21}
22
23impl Semigroupoid for FnA {
24    fn compose<A: Clone + 'static, B: Clone + 'static, C: Clone + 'static>(
25        f: Box<dyn Fn(B) -> C>,
26        g: Box<dyn Fn(A) -> B>,
27    ) -> Box<dyn Fn(A) -> C> {
28        Box::new(move |a| f(g(a)))
29    }
30}
31
32impl Category for FnA {
33    fn id<A: Clone + 'static>() -> Box<dyn Fn(A) -> A> {
34        Box::new(|a| a)
35    }
36}
37
38impl Arrow for FnA {
39    fn arr<A: Clone + 'static, B: Clone + 'static>(
40        f: impl Fn(A) -> B + 'static,
41    ) -> Box<dyn Fn(A) -> B> {
42        Box::new(f)
43    }
44
45    fn first<A: Clone + 'static, B: Clone + 'static, C: Clone + 'static>(
46        pab: Box<dyn Fn(A) -> B>,
47    ) -> Box<dyn Fn((A, C)) -> (B, C)> {
48        Box::new(move |(a, c)| (pab(a), c))
49    }
50
51    fn second<A: Clone + 'static, B: Clone + 'static, C: Clone + 'static>(
52        pab: Box<dyn Fn(A) -> B>,
53    ) -> Box<dyn Fn((C, A)) -> (C, B)> {
54        Box::new(move |(c, a)| (c, pab(a)))
55    }
56}
57
58impl ArrowChoice for FnA {
59    fn left<A: Clone + 'static, B: Clone + 'static, C: Clone + 'static>(
60        pab: Box<dyn Fn(A) -> B>,
61    ) -> Box<dyn Fn(Result<A, C>) -> Result<B, C>> {
62        Box::new(move |r| match r {
63            Ok(a) => Ok(pab(a)),
64            Err(c) => Err(c),
65        })
66    }
67
68    fn right<A: Clone + 'static, B: Clone + 'static, C: Clone + 'static>(
69        pab: Box<dyn Fn(A) -> B>,
70    ) -> Box<dyn Fn(Result<C, A>) -> Result<C, B>> {
71        Box::new(move |r| match r {
72            Ok(c) => Ok(c),
73            Err(a) => Err(pab(a)),
74        })
75    }
76}
77
78impl ArrowApply for FnA {
79    fn app<A: Clone + 'static, B: Clone + 'static>() -> Box<dyn Fn((Box<dyn Fn(A) -> B>, A)) -> B> {
80        Box::new(|(f, a)| f(a))
81    }
82}
83
84impl ArrowLoop for FnA {
85    fn loop_arrow<A: Clone + 'static, B: Clone + 'static, D: Default + Clone + 'static>(
86        f: Box<dyn Fn((A, D)) -> (B, D)>,
87    ) -> Box<dyn Fn(A) -> B> {
88        Box::new(move |a| {
89            let (b, _d) = f((a, D::default()));
90            b
91        })
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn fna_compose() {
101        let f: Box<dyn Fn(i32) -> i32> = Box::new(|x| x + 1);
102        let g: Box<dyn Fn(i32) -> i32> = Box::new(|x| x * 2);
103        let fg = FnA::compose(f, g);
104        assert_eq!(fg(3), 7); // (3 * 2) + 1
105    }
106
107    #[test]
108    fn fna_id() {
109        let id = FnA::id::<i32>();
110        assert_eq!(id(42), 42);
111    }
112
113    #[test]
114    fn fna_arr() {
115        let f = FnA::arr(|x: i32| x.to_string());
116        assert_eq!(f(42), "42");
117    }
118
119    #[test]
120    fn fna_first() {
121        let double: Box<dyn Fn(i32) -> i32> = Box::new(|x| x * 2);
122        let f = FnA::first::<i32, i32, &str>(double);
123        assert_eq!(f((5, "hi")), (10, "hi"));
124    }
125
126    #[test]
127    fn fna_second() {
128        let double: Box<dyn Fn(i32) -> i32> = Box::new(|x| x * 2);
129        let f = FnA::second::<i32, i32, &str>(double);
130        assert_eq!(f(("hi", 5)), ("hi", 10));
131    }
132
133    #[test]
134    fn fna_split() {
135        let double: Box<dyn Fn(i32) -> i32> = Box::new(|x| x * 2);
136        let negate: Box<dyn Fn(i32) -> i32> = Box::new(|x| -x);
137        let f = FnA::split(double, negate);
138        assert_eq!(f((3, 4)), (6, -4));
139    }
140
141    #[test]
142    fn fna_fanout() {
143        let double: Box<dyn Fn(i32) -> i32> = Box::new(|x| x * 2);
144        let negate: Box<dyn Fn(i32) -> i32> = Box::new(|x| -x);
145        let f = FnA::fanout(double, negate);
146        assert_eq!(f(5), (10, -5));
147    }
148
149    #[test]
150    fn fna_left() {
151        let double: Box<dyn Fn(i32) -> i32> = Box::new(|x| x * 2);
152        let f = FnA::left::<i32, i32, &str>(double);
153        assert_eq!(f(Ok(5)), Ok(10));
154        assert_eq!(f(Err("nope")), Err("nope"));
155    }
156
157    #[test]
158    fn fna_right() {
159        let double: Box<dyn Fn(i32) -> i32> = Box::new(|x| x * 2);
160        let f = FnA::right::<i32, i32, &str>(double);
161        assert_eq!(f(Err(5)), Err(10));
162        assert_eq!(f(Ok("yep")), Ok("yep"));
163    }
164
165    #[test]
166    fn fna_splat() {
167        let double: Box<dyn Fn(i32) -> i32> = Box::new(|x| x * 2);
168        let len: Box<dyn Fn(String) -> usize> = Box::new(|s| s.len());
169        let f = FnA::splat(double, len);
170        assert_eq!(f(Ok(5)), Ok(10));
171        assert_eq!(f(Err("hello".to_string())), Err(5));
172    }
173
174    #[test]
175    fn fna_fanin() {
176        let double: Box<dyn Fn(i32) -> String> = Box::new(|x| format!("int:{}", x));
177        let show: Box<dyn Fn(bool) -> String> = Box::new(|b| format!("bool:{}", b));
178        let f = FnA::fanin(double, show);
179        assert_eq!(f(Ok(42)), "int:42");
180        assert_eq!(f(Err(true)), "bool:true");
181    }
182
183    #[test]
184    fn fna_app() {
185        let app = FnA::app::<i32, i32>();
186        let double: Box<dyn Fn(i32) -> i32> = Box::new(|x| x * 2);
187        assert_eq!(app((double, 5)), 10);
188    }
189
190    #[test]
191    fn fna_loop_arrow() {
192        // loop_arrow feeds D::default() as the feedback value
193        let f = FnA::loop_arrow::<i32, i32, i32>(Box::new(|(a, d)| (a + d, d)));
194        assert_eq!(f(5), 5); // 5 + 0 (i32::default() == 0)
195    }
196}
197
198#[cfg(test)]
199mod law_tests {
200    use super::*;
201    use proptest::prelude::*;
202
203    proptest! {
204        // Semigroupoid associativity: compose(f, compose(g, h)) == compose(compose(f, g), h)
205        #[test]
206        fn associativity(x in any::<i16>()) {
207            let f = || -> Box<dyn Fn(i16) -> i16> { Box::new(|a| a.wrapping_add(1)) };
208            let g = || -> Box<dyn Fn(i16) -> i16> { Box::new(|a| a.wrapping_mul(2)) };
209            let h = || -> Box<dyn Fn(i16) -> i16> { Box::new(|a| a.wrapping_sub(3)) };
210
211            let left = FnA::compose(f(), FnA::compose(g(), h()));
212            let right = FnA::compose(FnA::compose(f(), g()), h());
213            prop_assert_eq!(left(x), right(x));
214        }
215
216        // Category left identity: compose(id(), f) == f
217        #[test]
218        fn left_identity(x in any::<i16>()) {
219            let f = || -> Box<dyn Fn(i16) -> i16> { Box::new(|a| a.wrapping_mul(2)) };
220            let left = FnA::compose(FnA::id(), f());
221            let right = f();
222            prop_assert_eq!(left(x), right(x));
223        }
224
225        // Category right identity: compose(f, id()) == f
226        #[test]
227        fn right_identity(x in any::<i16>()) {
228            let f = || -> Box<dyn Fn(i16) -> i16> { Box::new(|a| a.wrapping_mul(2)) };
229            let left = FnA::compose(f(), FnA::id());
230            let right = f();
231            prop_assert_eq!(left(x), right(x));
232        }
233
234        // Arrow: arr(id) == id()
235        #[test]
236        fn arr_id(x in any::<i16>()) {
237            let left = FnA::arr(|a: i16| a);
238            let right = FnA::id::<i16>();
239            prop_assert_eq!(left(x), right(x));
240        }
241
242        // Arrow: arr(g . f) == compose(arr(g), arr(f))
243        #[test]
244        fn arr_composition(x in any::<i16>()) {
245            let f = |a: i16| a.wrapping_add(1);
246            let g = |a: i16| a.wrapping_mul(2);
247
248            let left = FnA::arr(move |a: i16| g(f(a)));
249            let right = FnA::compose(FnA::arr(g), FnA::arr(f));
250            prop_assert_eq!(left(x), right(x));
251        }
252
253        // Arrow: first(arr(f)) == arr(|(a, c)| (f(a), c))
254        #[test]
255        fn first_arr(x in any::<i16>(), c in any::<i16>()) {
256            let f = |a: i16| a.wrapping_add(1);
257            let left = FnA::first::<i16, i16, i16>(FnA::arr(f));
258            let right = FnA::arr(move |(a, c): (i16, i16)| (f(a), c));
259            prop_assert_eq!(left((x, c)), right((x, c)));
260        }
261
262        // Arrow: first(compose(f, g)) == compose(first(f), first(g))
263        #[test]
264        fn first_compose(x in any::<i16>(), c in any::<i16>()) {
265            let f = || -> Box<dyn Fn(i16) -> i16> { Box::new(|a| a.wrapping_add(1)) };
266            let g = || -> Box<dyn Fn(i16) -> i16> { Box::new(|a| a.wrapping_mul(2)) };
267
268            let left = FnA::first::<i16, i16, i16>(FnA::compose(f(), g()));
269            let right = FnA::compose(
270                FnA::first::<i16, i16, i16>(f()),
271                FnA::first::<i16, i16, i16>(g()),
272            );
273            prop_assert_eq!(left((x, c)), right((x, c)));
274        }
275
276        // ArrowChoice: left(arr(f)) == arr(|r| r.map(f))
277        #[test]
278        fn left_arr(x in any::<Result<i16, i16>>()) {
279            let f = |a: i16| a.wrapping_add(1);
280            let left = FnA::left::<i16, i16, i16>(FnA::arr(f));
281            let right = FnA::arr(move |r: Result<i16, i16>| r.map(f));
282            prop_assert_eq!(left(x), right(x));
283        }
284    }
285}