Skip to main content

effectful/foundation/
func.rs

1//! Higher-order function utilities — mirrors Effect.ts `Function` namespace.
2//!
3//! Provides the functional building blocks (`identity`, `const_`, `flip`, `compose`,
4//! `pipe`, `absurd`, `tupled`, `untupled`) that the rest of Effect.ts relies on to
5//! express point-free style, lazy defaults, and function composition.
6
7/// `Function.identity` — returns its argument unchanged.
8///
9/// ```rust
10/// use effectful::func::identity;
11/// assert_eq!(identity(42_i32), 42);
12/// ```
13pub fn identity<A>(a: A) -> A {
14  a
15}
16
17/// `Function.constFn` — returns a function that always returns `value`, ignoring its argument.
18///
19/// Named `const_` to avoid collision with the Rust `const` keyword.
20///
21/// ```rust
22/// use effectful::func::const_;
23/// let always_five = const_(5_i32);
24/// assert_eq!(always_five(()), 5);
25/// ```
26pub fn const_<A: Clone>(value: A) -> impl Fn(()) -> A + Clone {
27  move |_| value.clone()
28}
29
30/// Like [`const_`] but the returned closure accepts a single argument of any type and ignores it.
31///
32/// ```rust
33/// use effectful::func::always;
34/// let always_true = always(true);
35/// assert!(always_true(0_i32));
36/// assert!(always_true(1_i32));
37/// ```
38pub fn always<A: Clone, B>(value: A) -> impl Fn(B) -> A + Clone {
39  move |_| value.clone()
40}
41
42/// `Function.flip` — swap the first two arguments of a two-argument function.
43///
44/// ```rust
45/// use effectful::func::flip;
46/// let sub = |a: i32, b: i32| a - b;
47/// let flipped = flip(sub);
48/// assert_eq!(flipped(3, 10), 10 - 3);
49/// ```
50pub fn flip<A, B, C>(f: impl Fn(A, B) -> C) -> impl Fn(B, A) -> C {
51  move |b, a| f(a, b)
52}
53
54/// `Function.compose` — right-to-left function composition: `compose(f, g)(x) == f(g(x))`.
55///
56/// ```rust
57/// use effectful::func::compose;
58/// let add_one = |n: i32| n + 1;
59/// let double  = |n: i32| n * 2;
60/// let double_then_inc = compose(add_one, double);
61/// assert_eq!(double_then_inc(3), 7); // (3*2)+1
62/// ```
63pub fn compose<A, B, C>(f: impl Fn(B) -> C, g: impl Fn(A) -> B) -> impl Fn(A) -> C {
64  move |a| f(g(a))
65}
66
67/// `Function.pipe` (unary) — apply `f` to `a`.  Useful for point-free pipelines.
68///
69/// For multi-step pipelines prefer the [`pipe!`](macro@effectful_macro::pipe) macro.
70///
71/// ```rust
72/// use effectful::func::pipe1;
73/// assert_eq!(pipe1(5_i32, |n| n * 2), 10);
74/// ```
75pub fn pipe1<A, B>(a: A, f: impl FnOnce(A) -> B) -> B {
76  f(a)
77}
78
79/// `Function.pipe` two steps.
80pub fn pipe2<A, B, C>(a: A, f: impl FnOnce(A) -> B, g: impl FnOnce(B) -> C) -> C {
81  g(f(a))
82}
83
84/// `Function.pipe` three steps.
85pub fn pipe3<A, B, C, D>(
86  a: A,
87  f: impl FnOnce(A) -> B,
88  g: impl FnOnce(B) -> C,
89  h: impl FnOnce(C) -> D,
90) -> D {
91  h(g(f(a)))
92}
93
94/// `Function.absurd` — a function whose input is the uninhabited `!` (never) type.
95///
96/// Call this in branches that are statically unreachable.
97///
98/// ```rust,ignore
99/// use effectful::func::absurd;
100/// fn demo(x: std::convert::Infallible) -> i32 {
101///     match x {}
102/// }
103/// ```
104pub fn absurd<A>(never: std::convert::Infallible) -> A {
105  match never {}
106}
107
108/// `Function.tupled` — convert a two-argument function into a function that takes a tuple.
109///
110/// ```rust
111/// use effectful::func::tupled;
112/// let add = |a: i32, b: i32| a + b;
113/// let tupled_add = tupled(add);
114/// assert_eq!(tupled_add((3, 4)), 7);
115/// ```
116pub fn tupled<A, B, C>(f: impl Fn(A, B) -> C) -> impl Fn((A, B)) -> C {
117  move |(a, b)| f(a, b)
118}
119
120/// `Function.untupled` — convert a function that takes a tuple into a two-argument function.
121///
122/// ```rust
123/// use effectful::func::untupled;
124/// let sum_pair = |(a, b): (i32, i32)| a + b;
125/// let two_arg = untupled(sum_pair);
126/// assert_eq!(two_arg(3, 4), 7);
127/// ```
128pub fn untupled<A, B, C>(f: impl Fn((A, B)) -> C) -> impl Fn(A, B) -> C {
129  move |a, b| f((a, b))
130}
131
132/// Memoize a single-argument function with a `HashMap` cache (eagerly clones the key).
133///
134/// ```rust
135/// use effectful::func::memoize;
136/// let mut double = memoize(|n: i32| n * 2);
137/// assert_eq!(double(3), 6);
138/// assert_eq!(double(3), 6); // cached
139/// ```
140pub fn memoize<A, B>(f: impl Fn(A) -> B) -> impl FnMut(A) -> B
141where
142  A: std::hash::Hash + Eq + Clone,
143  B: Clone,
144{
145  let mut cache = std::collections::HashMap::<A, B>::new();
146  move |a: A| {
147    if let Some(v) = cache.get(&a) {
148      return v.clone();
149    }
150    let v = f(a.clone());
151    cache.insert(a, v.clone());
152    v
153  }
154}
155
156// ── Tests ─────────────────────────────────────────────────────────────────────
157
158#[cfg(test)]
159mod tests {
160  use super::*;
161  use rstest::rstest;
162
163  // ── identity ──────────────────────────────────────────────────────────────
164
165  mod identity_tests {
166    use super::*;
167
168    #[rstest]
169    #[case::integer(42_i32)]
170    #[case::zero(0_i32)]
171    #[case::negative(-7_i32)]
172    fn identity_returns_input(#[case] v: i32) {
173      assert_eq!(identity(v), v);
174    }
175
176    #[test]
177    fn identity_works_for_strings() {
178      let s = "hello".to_string();
179      assert_eq!(identity(s.clone()), s);
180    }
181
182    #[test]
183    fn identity_works_for_options() {
184      assert_eq!(identity(Some(1_i32)), Some(1));
185    }
186  }
187
188  // ── const_ / always ───────────────────────────────────────────────────────
189
190  mod const_tests {
191    use super::*;
192
193    #[test]
194    fn const_always_returns_same_value() {
195      let f = const_(99_i32);
196      assert_eq!(f(()), 99);
197      assert_eq!(f(()), 99);
198    }
199
200    #[test]
201    fn always_ignores_argument() {
202      let f = always::<bool, i32>(true);
203      assert!(f(0));
204      assert!(f(42));
205    }
206
207    #[test]
208    fn always_with_string_value() {
209      let f = always::<&str, i32>("fixed");
210      assert_eq!(f(100), "fixed");
211    }
212  }
213
214  // ── flip ─────────────────────────────────────────────────────────────────
215
216  mod flip_tests {
217    use super::*;
218
219    #[test]
220    fn flip_reverses_arguments() {
221      let sub = |a: i32, b: i32| a - b;
222      let flipped = flip(sub);
223      assert_eq!(flipped(3, 10), 10 - 3);
224    }
225
226    #[test]
227    fn flip_of_flip_is_original() {
228      let f = |a: i32, b: i32| a * 10 + b;
229      // flip(flip(f)) behaves like f
230      let result = flip(flip(|a: i32, b: i32| a * 10 + b))(1, 2);
231      assert_eq!(result, f(1, 2));
232    }
233
234    #[rstest]
235    #[case(10_i32, 3_i32, -7_i32)]
236    #[case(5_i32, 5_i32, 0_i32)]
237    #[case(0_i32, 1_i32, 1_i32)]
238    fn flip_sub(#[case] b: i32, #[case] a: i32, #[case] expected: i32) {
239      // flip(a - b)(b_arg, a_arg) == a_arg - b_arg
240      let flipped_sub = flip(|a: i32, b: i32| a - b);
241      assert_eq!(flipped_sub(b, a), expected);
242    }
243  }
244
245  // ── compose ───────────────────────────────────────────────────────────────
246
247  mod compose_tests {
248    use super::*;
249
250    #[test]
251    fn compose_applies_right_then_left() {
252      let add_one = |n: i32| n + 1;
253      let double = |n: i32| n * 2;
254      let composed = compose(add_one, double);
255      assert_eq!(composed(3), 7); // 3*2 then +1
256    }
257
258    #[test]
259    fn compose_with_identity_is_identity() {
260      let double = |n: i32| n * 2;
261      let composed = compose(identity, double);
262      assert_eq!(composed(5), 10);
263    }
264
265    #[rstest]
266    #[case(0_i32, 1_i32)]
267    #[case(1_i32, 3_i32)]
268    #[case(2_i32, 5_i32)]
269    fn compose_double_plus_one(#[case] input: i32, #[case] expected: i32) {
270      let f = compose(|n: i32| n + 1, |n: i32| n * 2);
271      assert_eq!(f(input), expected);
272    }
273  }
274
275  // ── pipe1 / pipe2 / pipe3 ─────────────────────────────────────────────────
276
277  mod pipe_tests {
278    use super::*;
279
280    #[test]
281    fn pipe1_applies_single_function() {
282      assert_eq!(pipe1(5_i32, |n| n * 3), 15);
283    }
284
285    #[test]
286    fn pipe2_applies_two_functions_left_to_right() {
287      assert_eq!(pipe2(3_i32, |n| n * 2, |n| n + 1), 7);
288    }
289
290    #[test]
291    fn pipe3_applies_three_functions_left_to_right() {
292      assert_eq!(
293        pipe3(2_i32, |n| n + 1, |n| n * 2, |n| n - 1),
294        5 // (2+1)*2 - 1 = 5
295      );
296    }
297
298    #[test]
299    fn pipe1_with_string_conversion() {
300      assert_eq!(pipe1(42_i32, |n| n.to_string()), "42");
301    }
302  }
303
304  // ── absurd ────────────────────────────────────────────────────────────────
305  // (cannot construct Infallible in tests, but we can verify the signature compiles)
306
307  // ── tupled / untupled ─────────────────────────────────────────────────────
308
309  mod tupled_tests {
310    use super::*;
311
312    #[test]
313    fn tupled_converts_two_arg_to_tuple_arg() {
314      let add = |a: i32, b: i32| a + b;
315      let tupled_add = tupled(add);
316      assert_eq!(tupled_add((3, 4)), 7);
317    }
318
319    #[test]
320    fn untupled_converts_tuple_arg_to_two_arg() {
321      let sum_pair = |(a, b): (i32, i32)| a + b;
322      let two_arg = untupled(sum_pair);
323      assert_eq!(two_arg(3, 4), 7);
324    }
325
326    #[test]
327    fn tupled_then_untupled_is_original() {
328      let f = |a: i32, b: i32| a - b;
329      let roundtrip = untupled(tupled(f));
330      assert_eq!(roundtrip(10, 3), 7);
331    }
332
333    #[rstest]
334    #[case(1_i32, 2_i32, 3_i32)]
335    #[case(0_i32, 0_i32, 0_i32)]
336    #[case(-1_i32, 1_i32, 0_i32)]
337    fn tupled_add_cases(#[case] a: i32, #[case] b: i32, #[case] expected: i32) {
338      let f = tupled(|a: i32, b: i32| a + b);
339      assert_eq!(f((a, b)), expected);
340    }
341  }
342
343  // ── memoize ───────────────────────────────────────────────────────────────
344
345  mod memoize_tests {
346    use super::*;
347
348    #[test]
349    fn memoize_returns_correct_value() {
350      let double = memoize(|n: i32| n * 2);
351      // (memoize returns FnMut, bind to mut)
352      let mut double = double;
353      assert_eq!(double(5), 10);
354    }
355
356    #[test]
357    fn memoize_returns_same_value_on_second_call() {
358      let mut f = memoize(|n: i32| n + 100);
359      assert_eq!(f(3), 103);
360      assert_eq!(f(3), 103);
361    }
362
363    #[test]
364    fn memoize_caches_independently_per_key() {
365      let mut f = memoize(|s: &str| s.len());
366      assert_eq!(f("hi"), 2);
367      assert_eq!(f("hello"), 5);
368      assert_eq!(f("hi"), 2);
369    }
370  }
371}