๐Ÿฆ€ Functional Rust

237: Day Convolution

Difficulty: โญโญโญโญ Level: Category Theory Combine two functors into a new functor by pairing their contents with a combining function โ€” the foundation of applicative functors.

The Problem This Solves

You have two containers (functors) and you want to combine their contents in all possible ways. `Vec<fn(A)->B>` applied to `Vec<A>` should give `Vec<B>` โ€” every function applied to every element. `Option<i32>` combined with `Option<i32>` via addition gives `Option<i32>` โ€” `Some` only if both are `Some`. But what's the general structure behind all these "combining two functors" patterns? Day convolution is that structure. It says: to combine functors `F` and `G`, hold three things together: a value from `F<B>`, a value from `G<C>`, and a function `B -> C -> A`. The result is a value that represents "all ways to combine one element from F and one element from G using the combining function." Run it (`lower()`) and you get the concrete `A` values. This is the formal foundation of applicative functors: a functor is applicative (has `<*>`) if and only if Day convolution over it forms a monoid. In practice, this explains why `Vec`'s applicative is the cartesian product and `Option`'s applicative is the `zip` (both present = result, otherwise nothing).

The Intuition

Convolution in signal processing: to combine two signals, you slide one over the other and sum the products at each offset. Day convolution is the categorical analogue: to combine two functors, you pair all elements from one with all elements from the other via a combining function. Concrete: `Day(Vec<B>, Vec<C>, B -> C -> A)` means "for every `b` in the left Vec and every `c` in the right Vec, apply the combiner to get an `A`." Lower it and you get `Vec<A>` containing all `bร—c` combinations. For `Option`: `Day(Some(3), Some(4), +)` = `Some(7)`. `Day(Some(3), None, +)` = `None`. Because with `Option`, "all combinations" means 0 combinations if either side is empty. The "monoidal" structure: Day convolution is associative โ€” `Day(Day(F, G), H) โ‰… Day(F, Day(G, H))`. And there's a unit: the identity functor (just wrapping a value). A monoid in the category of functors under Day convolution = an applicative functor. That's the theorem.

How It Works in Rust

/// Day convolution: holds two functor values and a combining function
pub struct Day<B, C, A> {
 left:    Vec<B>,
 right:   Vec<C>,
 combine: Box<dyn Fn(B, C) -> A>,
}

impl<B: Clone, C: Clone, A> Day<B, C, A> {
 pub fn new(left: Vec<B>, right: Vec<C>, combine: impl Fn(B, C) -> A + 'static) -> Self {
     Day { left, right, combine: Box::new(combine) }
 }

 /// Lower: compute the cartesian product applying the combiner
 pub fn lower(self) -> Vec<A> {
     let mut result = Vec::new();
     for b in &self.left {
         for c in &self.right {
             result.push((self.combine)(b.clone(), c.clone()));
         }
     }
     result  // all combinations
 }

 /// fmap: post-compose a function on the result type
 pub fn fmap<D>(self, f: impl Fn(A) -> D + 'static) -> Day<B, C, D> {
     let old_combine = self.combine;
     Day {
         left: self.left,
         right: self.right,
         combine: Box::new(move |b, c| f(old_combine(b, c))),
     }
 }
}
The list applicative IS Day convolution:
// Apply list of functions to list of values โ€” this is exactly Day
fn apply_list<A: Clone, B>(fs: &[fn(A) -> B], xs: &[A]) -> Vec<B> {
 fs.iter().flat_map(|f| xs.iter().map(|x| f(x.clone()))).collect()
 // Same as Day::new(fs.to_vec(), xs.to_vec(), |f, x| f(x)).lower()
}

// Option convolution โ€” both must be Some
fn day_option<B, C, A>(lb: Option<B>, rc: Option<C>, f: impl Fn(B, C) -> A) -> Option<A> {
 lb.zip(rc).map(|(b, c)| f(b, c))
}

What This Unlocks

Key Differences

ConceptOCamlRust
Day typeGADT or existential moduleStruct with `Box<dyn Fn>`
`lower`Fold via module interfaceNested loops, explicit clone
List applicative`List.concat_map``flat_map` / `Day::lower`
Option applicative`Option.bind` / custom`.zip().map()`
Unit functorIdentity moduleImplicit (wrapping `A` directly)
/// Day Convolution: combining two functors into an applicative.
///
/// Day f g a = โˆƒb c. (f b, g c, b -> c -> a)
///
/// The Day convolution of two functors `f` and `g` yields a new functor,
/// and if both `f` and `g` are monoidal (have a "unit" and "multiply"),
/// then Day(f, g) is an applicative functor.
///
/// Concrete example: Day(Vec, Vec) = cartesian product (list applicative)
///   Day List List a = all ways to combine elements from two lists
///
/// This is the foundation of many applicative structures and is used in
/// profunctor optics and the `Conjoined` class in Haskell's `lens` library.

/// Concrete Day convolution over Vec:
/// `Day<B, C, A>` holds a `Vec<B>`, a `Vec<C>`, and a combiner `B -> C -> A`.
/// This represents "all ways to combine one element from each list."
pub struct Day<B, C, A> {
    left: Vec<B>,
    right: Vec<C>,
    combine: Box<dyn Fn(B, C) -> A>,
}

impl<B: Clone + 'static, C: Clone + 'static, A: 'static> Day<B, C, A> {
    pub fn new(left: Vec<B>, right: Vec<C>, combine: impl Fn(B, C) -> A + 'static) -> Self {
        Day {
            left,
            right,
            combine: Box::new(combine),
        }
    }

    /// Lower: compute the cartesian product applying the combiner.
    pub fn lower(self) -> Vec<A> {
        let mut result = Vec::new();
        for b in &self.left {
            for c in &self.right {
                result.push((self.combine)(b.clone(), c.clone()));
            }
        }
        result
    }

    /// fmap: post-compose a function on the result type A.
    pub fn fmap<D: 'static>(self, f: impl Fn(A) -> D + 'static) -> Day<B, C, D>
    where
        B: 'static,
        C: 'static,
    {
        let old_combine = self.combine;
        Day {
            left: self.left,
            right: self.right,
            combine: Box::new(move |b, c| f(old_combine(b, c))),
        }
    }
}

/// Day convolution as the list applicative.
/// `apply_list fs xs` applies each function in `fs` to each element in `xs`.
/// This is exactly the Day convolution: Day List List a
fn apply_list<A: Clone, B: Clone>(fs: &[impl Fn(A) -> B], xs: &[A]) -> Vec<B> {
    let mut result = Vec::new();
    for f in fs {
        for x in xs {
            result.push(f(x.clone()));
        }
    }
    result
}

/// General Day product: given `Vec<B>`, `Vec<C>`, and combiner `B -> C -> A`
fn day_product<B: Clone, C: Clone, A>(
    bs: &[B],
    cs: &[C],
    f: impl Fn(B, C) -> A,
) -> Vec<A> {
    let mut result = Vec::new();
    for b in bs {
        for c in cs {
            result.push(f(b.clone(), c.clone()));
        }
    }
    result
}

/// Day convolution for option types:
/// `Day<Option<B>, Option<C>, A>` โ€” combines two optional values.
fn day_option<B: Clone, C: Clone, A>(
    ob: Option<B>,
    oc: Option<C>,
    f: impl Fn(B, C) -> A,
) -> Option<A> {
    match (ob, oc) {
        (Some(b), Some(c)) => Some(f(b, c)),
        _ => None,
    }
}

fn main() {
    println!("=== Day Convolution ===\n");
    println!("Day f g a = โˆƒb c. (f b, g c, b -> c -> a)");
    println!("Fundamental operation for building applicative functors.\n");

    // Basic Day convolution: all sums
    let xs = vec![1_i32, 2, 3];
    let ys = vec![10_i32, 20];

    let sums = day_product(&xs, &ys, |a, b| a + b);
    println!("All sums [1,2,3] ร— [10,20]: {:?}", sums);

    let products = day_product(&xs, &ys, |a, b| a * b);
    println!("All products:               {:?}", products);

    let pairs = day_product(&xs, &ys, |a, b| (a, b));
    println!("Cartesian product:          {:?}", pairs);

    println!();

    // Day as the list applicative: apply functions to values
    let fs: Vec<Box<dyn Fn(i32) -> i32>> = vec![
        Box::new(|x| x * 2),
        Box::new(|x| x + 100),
    ];
    let applied = {
        let mut result = Vec::new();
        for f in &fs {
            for &x in &xs {
                result.push(f(x));
            }
        }
        result
    };
    println!("List applicative [*2, +100] <*> [1,2,3]: {:?}", applied);

    // Day for options
    let day_opt_both = day_option(Some(3_i32), Some(4_i32), |a, b| a + b);
    let day_opt_missing = day_option(Some(3_i32), None::<i32>, |a, b| a + b);
    println!();
    println!("Day Option (Some(3), Some(4), +): {:?}", day_opt_both);
    println!("Day Option (Some(3), None,    +): {:?}", day_opt_missing);

    // Full Day struct with fmap
    println!();
    let d = Day::new(vec![1_i32, 2], vec![10_i32, 20], |a, b| a + b);
    let d2 = d.fmap(|x| x * 10);
    let result = d2.lower();
    println!("Day struct with fmap (*10): {:?}", result);

    println!();
    println!("Day convolution is associative and has a unit (the identity functor).");
    println!("Monoid in the category of functors under Day = Applicative functor.");
    println!("apply_list = Day List List applied to (fs, xs) via function application.");
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_day_product_sums() {
        let result = day_product(&[1, 2], &[10, 100], |a: i32, b: i32| a + b);
        assert_eq!(result, vec![11, 101, 12, 102]);
    }

    #[test]
    fn test_day_option_both_some() {
        let result = day_option(Some(3_i32), Some(4), |a, b| a * b);
        assert_eq!(result, Some(12));
    }

    #[test]
    fn test_day_option_none() {
        let result: Option<i32> = day_option(None, Some(4_i32), |a, b: i32| a + b);
        assert_eq!(result, None);
    }

    #[test]
    fn test_day_struct_lower() {
        let d = Day::new(vec![1_i32, 2], vec![3_i32, 4], |a, b| a + b);
        let result = d.lower();
        assert_eq!(result, vec![4, 5, 5, 6]);
    }

    #[test]
    fn test_apply_list() {
        let fs: &[fn(i32) -> i32] = &[|x| x * 2, |x| x + 1];
        let result = apply_list(fs, &[1, 2, 3]);
        assert_eq!(result, vec![2, 4, 6, 2, 3, 4]);
    }
}
(* Day convolution: a way to combine two functors.
   Day f g a = exists b c. f b * g c * (b -> c -> a)
   Used to build applicative functors compositionally. *)

(* Simplified Day convolution for lists *)
(* Day List List a = all ways to combine elements *)
type ('f, 'g, 'a) day =
  | Day : 'f * 'g * ('b -> 'c -> 'a) -> ('f, 'g, 'a) day

(* Day convolution of two lists = cartesian product *)
let day_product xs ys f =
  List.concat_map (fun x ->
    List.map (fun y -> f x y) ys
  ) xs

(* This IS the applicative for lists *)
let apply_list fs xs = day_product fs xs (fun f x -> f x)

let () =
  let xs = [1; 2; 3] in
  let ys = [10; 20] in

  (* Day convolution: all sums *)
  let sums = day_product xs ys ( + ) in
  Printf.printf "all sums: [%s]\n" (sums |> List.map string_of_int |> String.concat ";");

  (* All products *)
  let prods = day_product xs ys ( * ) in
  Printf.printf "all products: [%s]\n" (prods |> List.map string_of_int |> String.concat ";");

  (* Applicative from Day *)
  let fs = [(fun x -> x * 2); (fun x -> x + 1)] in
  let results = apply_list fs [1; 2; 3] in
  Printf.printf "apply: [%s]\n" (results |> List.map string_of_int |> String.concat ";")