Skip to main content

use_algebra/
laws.rs

1//! Finite-sample algebraic law helpers.
2
3/// Returns whether applying `operation` to any pair of values in `elements`
4/// yields another member of `elements`.
5#[must_use]
6pub fn is_closed_under<T, F>(elements: &[T], operation: F) -> bool
7where
8    T: Copy + PartialEq,
9    F: Fn(T, T) -> T + Copy,
10{
11    for left in elements.iter().copied() {
12        for right in elements.iter().copied() {
13            let result = operation(left, right);
14
15            if !elements.contains(&result) {
16                return false;
17            }
18        }
19    }
20
21    true
22}
23
24/// Returns whether `operation` is associative over `elements`.
25#[must_use]
26pub fn is_associative<T, F>(elements: &[T], operation: F) -> bool
27where
28    T: Copy + PartialEq,
29    F: Fn(T, T) -> T + Copy,
30{
31    for first in elements.iter().copied() {
32        for second in elements.iter().copied() {
33            for third in elements.iter().copied() {
34                if operation(operation(first, second), third)
35                    != operation(first, operation(second, third))
36                {
37                    return false;
38                }
39            }
40        }
41    }
42
43    true
44}
45
46/// Returns whether `operation` is commutative over `elements`.
47#[must_use]
48pub fn is_commutative<T, F>(elements: &[T], operation: F) -> bool
49where
50    T: Copy + PartialEq,
51    F: Fn(T, T) -> T + Copy,
52{
53    for left in elements.iter().copied() {
54        for right in elements.iter().copied() {
55            if operation(left, right) != operation(right, left) {
56                return false;
57            }
58        }
59    }
60
61    true
62}
63
64/// Returns a two-sided identity element for `operation`, if one exists in
65/// `elements`.
66#[must_use]
67pub fn identity_element<T, F>(elements: &[T], operation: F) -> Option<T>
68where
69    T: Copy + PartialEq,
70    F: Fn(T, T) -> T + Copy,
71{
72    'candidate: for candidate in elements.iter().copied() {
73        for value in elements.iter().copied() {
74            if operation(candidate, value) != value || operation(value, candidate) != value {
75                continue 'candidate;
76            }
77        }
78
79        return Some(candidate);
80    }
81
82    None
83}
84
85/// Returns whether every value in `elements` has a two-sided inverse with
86/// respect to `identity` and `operation`.
87#[must_use]
88pub fn has_inverses<T, F>(elements: &[T], operation: F, identity: T) -> bool
89where
90    T: Copy + PartialEq,
91    F: Fn(T, T) -> T + Copy,
92{
93    if !elements.contains(&identity) {
94        return false;
95    }
96
97    for value in elements.iter().copied() {
98        let mut found_inverse = false;
99
100        for candidate in elements.iter().copied() {
101            if operation(value, candidate) == identity && operation(candidate, value) == identity {
102                found_inverse = true;
103                break;
104            }
105        }
106
107        if !found_inverse {
108            return false;
109        }
110    }
111
112    true
113}
114
115/// Returns whether `elements` with `operation` form a monoid.
116#[must_use]
117pub fn is_monoid<T, F>(elements: &[T], operation: F) -> bool
118where
119    T: Copy + PartialEq,
120    F: Fn(T, T) -> T + Copy,
121{
122    is_closed_under(elements, operation)
123        && is_associative(elements, operation)
124        && identity_element(elements, operation).is_some()
125}
126
127/// Returns whether `elements` with `operation` form a group.
128#[must_use]
129pub fn is_group<T, F>(elements: &[T], operation: F) -> bool
130where
131    T: Copy + PartialEq,
132    F: Fn(T, T) -> T + Copy,
133{
134    let Some(identity) = identity_element(elements, operation) else {
135        return false;
136    };
137
138    is_closed_under(elements, operation)
139        && is_associative(elements, operation)
140        && has_inverses(elements, operation, identity)
141}
142
143/// Returns whether `elements` with `operation` form an abelian group.
144#[must_use]
145pub fn is_abelian_group<T, F>(elements: &[T], operation: F) -> bool
146where
147    T: Copy + PartialEq,
148    F: Fn(T, T) -> T + Copy,
149{
150    is_group(elements, operation) && is_commutative(elements, operation)
151}
152
153/// Returns whether `multiply` distributes over `add` from both sides on
154/// `elements`.
155#[must_use]
156pub fn is_distributive_over<T, Mul, Add>(elements: &[T], multiply: Mul, add: Add) -> bool
157where
158    T: Copy + PartialEq,
159    Mul: Fn(T, T) -> T + Copy,
160    Add: Fn(T, T) -> T + Copy,
161{
162    for left in elements.iter().copied() {
163        for middle in elements.iter().copied() {
164            for right in elements.iter().copied() {
165                if multiply(left, add(middle, right))
166                    != add(multiply(left, middle), multiply(left, right))
167                {
168                    return false;
169                }
170
171                if multiply(add(left, middle), right)
172                    != add(multiply(left, right), multiply(middle, right))
173                {
174                    return false;
175                }
176            }
177        }
178    }
179
180    true
181}
182
183/// Returns whether `elements` with `add` and `multiply` form a ring.
184#[must_use]
185pub fn is_ring<T, Add, Mul>(elements: &[T], add: Add, multiply: Mul) -> bool
186where
187    T: Copy + PartialEq,
188    Add: Fn(T, T) -> T + Copy,
189    Mul: Fn(T, T) -> T + Copy,
190{
191    is_abelian_group(elements, add)
192        && is_closed_under(elements, multiply)
193        && is_associative(elements, multiply)
194        && is_distributive_over(elements, multiply, add)
195}
196
197#[cfg(test)]
198mod tests {
199    use super::{
200        has_inverses, identity_element, is_abelian_group, is_associative, is_closed_under,
201        is_commutative, is_distributive_over, is_group, is_monoid, is_ring,
202    };
203
204    #[test]
205    fn validates_modular_group_and_ring_examples() {
206        let residues = [0_u8, 1, 2];
207        let add_mod_3 = |left, right| (left + right) % 3;
208        let mul_mod_3 = |left, right| (left * right) % 3;
209
210        assert!(is_closed_under(&residues, add_mod_3));
211        assert!(is_associative(&residues, add_mod_3));
212        assert!(is_commutative(&residues, add_mod_3));
213        assert_eq!(identity_element(&residues, add_mod_3), Some(0));
214        assert!(has_inverses(&residues, add_mod_3, 0));
215        assert!(is_monoid(&residues, add_mod_3));
216        assert!(is_group(&residues, add_mod_3));
217        assert!(is_abelian_group(&residues, add_mod_3));
218        assert!(is_distributive_over(&residues, mul_mod_3, add_mod_3));
219        assert!(is_ring(&residues, add_mod_3, mul_mod_3));
220    }
221
222    #[test]
223    fn rejects_operations_that_do_not_form_the_expected_structures() {
224        let booleans = [false, true];
225        let and = |left, right| left && right;
226        let values = [0_u8, 1];
227        let add = |left, right| left + right;
228
229        assert_eq!(identity_element(&booleans, and), Some(true));
230        assert!(is_monoid(&booleans, and));
231        assert!(!is_group(&booleans, and));
232        assert!(!is_closed_under(&values, add));
233        assert!(!is_ring(&values, add, add));
234    }
235}