|
1 | | -# cmp |
2 | | -Explicit equality & ordering lib for Gleam |
| 1 | +# cmp_gleam |
| 2 | + |
| 3 | +[](https://hex.pm/packages/cmp_gleam) |
| 4 | +[](https://hexdocs.pm/cmp_gleam/) |
| 5 | +[](https://github.com/lupodevelop/cmp/actions) |
| 6 | + |
| 7 | +cmp_gleam: explicit comparator helpers for Gleam |
| 8 | + |
| 9 | +cmp_gleam is a tiny, focused library for building small, composable comparators in Gleam. It intentionally avoids magic (no derives, no hidden behavior) so that comparison logic remains explicit, easy to test and easy to reason about. |
| 10 | + |
| 11 | +Key goals: |
| 12 | + |
| 13 | +- Keep the API small and predictable |
| 14 | +- Provide composable building blocks for lexicographic and tie-breaker ordering |
| 15 | +- Allow optional normalization/folding (for example using the `str` library) without adding runtime dependencies |
| 16 | + |
| 17 | +Why this approach? |
| 18 | + |
| 19 | +`cmp` favors explicitness over implicit derivation. By making comparators first-class and composable you get predictable behaviour, easier testing, and straightforward integration with normalization libraries when you need to handle real-world Unicode data. |
| 20 | + |
| 21 | +Install |
| 22 | + |
| 23 | +```sh |
| 24 | +gleam add cmp_gleam |
| 25 | +``` |
| 26 | + |
| 27 | +Quick examples 🔧 |
| 28 | + |
| 29 | +1. Sort integers |
| 30 | + |
| 31 | +```gleam |
| 32 | +import cmp |
| 33 | +import gleam/list |
| 34 | +
|
| 35 | +pub fn sort_ints(xs: List(Int)) -> List(Int) { |
| 36 | + list.sort(xs, by: cmp.natural_int) |
| 37 | +} |
| 38 | +``` |
| 39 | + |
| 40 | +2. Sort records by a string field (contramap) |
| 41 | + |
| 42 | +```gleam |
| 43 | +import cmp |
| 44 | +import gleam/list |
| 45 | +import gleam/string |
| 46 | +
|
| 47 | +type User { |
| 48 | + User(name: String, age: Int) |
| 49 | +} |
| 50 | +
|
| 51 | +pub fn sort_by_name(users: List(User)) -> List(User) { |
| 52 | + let cmp_name = cmp.by(fn(u) { case u { User(name, _) -> name } }, string.compare) |
| 53 | + list.sort(users, by: cmp_name) |
| 54 | +} |
| 55 | +``` |
| 56 | + |
| 57 | +3. Lexicographic ordering / tie-breakers (chain / then / lazy_then) |
| 58 | + |
| 59 | +```gleam |
| 60 | +let cmp = cmp.chain([ |
| 61 | + cmp.by(fn(u) { case u { User(name, _) -> name } }, string.compare), |
| 62 | + cmp.by(fn(u) { case u { User(_, age) -> age } }, cmp.natural_int) |
| 63 | +]) |
| 64 | +list.sort(users, by: cmp) |
| 65 | +``` |
| 66 | + |
| 67 | +4. Integrating with `str` for normalization (e.g. ASCII-fold) |
| 68 | + |
| 69 | +`str` is optional — `cmp` does not import it. You can pass `str` functions to `cmp` APIs such as `by_normalized_string` to fold/normalize before comparing: |
| 70 | + |
| 71 | +```gleam |
| 72 | +import cmp |
| 73 | +import gleam/list |
| 74 | +import gleam/string |
| 75 | +import str.extra |
| 76 | +
|
| 77 | +pub fn sort_by_name_ascii_fold(users: List(User)) -> List(User) { |
| 78 | + let normalize = str.extra.ascii_fold |
| 79 | + let cmp_name = cmp.by_normalized_string(fn(u) { case u { User(name, _) -> name } }, normalize, string.compare) |
| 80 | + list.sort(users, by: cmp_name) |
| 81 | +} |
| 82 | +``` |
| 83 | + |
| 84 | +You can also combine normalization steps (for example: fold + lowercase): |
| 85 | + |
| 86 | +```gleam |
| 87 | +let normalize = fn(s) { s |> str.extra.ascii_fold |> string.lowercase } |
| 88 | +``` |
| 89 | + |
| 90 | +5. Using metrics (similarity/distance) to order items (advanced example) |
| 91 | + |
| 92 | +⚠️ **Warning**: similarity/distance metrics don't guarantee transitivity (a < b and b < c doesn't always imply a < c). If you need a total order, sort by the metric value itself rather than using it directly as a comparator. |
| 93 | + |
| 94 | +```gleam |
| 95 | +import gleam/order |
| 96 | +
|
| 97 | +// Example: sort by similarity to a reference string (careful: not a total order!) |
| 98 | +let similarity_cmp = fn(a, b) { |
| 99 | + let s = str.similarity(a, b) |
| 100 | + case s > 0.8 { True -> order.Lt False -> order.Gt } |
| 101 | +} |
| 102 | +let cmp_sim = cmp.by(fn(u) { case u { User(name, _) -> name } }, similarity_cmp) |
| 103 | +``` |
| 104 | + |
| 105 | +Better approach for metrics: |
| 106 | + |
| 107 | +```gleam |
| 108 | +// Compute similarity as Float, then sort by that value |
| 109 | +let reference = "Alice" |
| 110 | +let with_similarity = list.map(users, fn(u) { |
| 111 | + let sim = str.similarity(u.name, reference) |
| 112 | + #(u, sim) |
| 113 | +}) |
| 114 | +let sorted = list.sort(with_similarity, by: cmp.by(fn(pair) { pair.1 }, float.compare)) |
| 115 | +``` |
| 116 | + |
| 117 | +Unicode & normalization notes ⚠️ |
| 118 | + |
| 119 | +- `string.compare` compares strings as-is; composed vs decomposed characters may behave differently if not normalized. |
| 120 | +- For user-facing sorting (names, titles), it is recommended to **normalize** (NFC/NFD) or apply folding (remove accents) before comparing. |
| 121 | +- `str` provides useful primitives (`str.extra.ascii_fold`, `str.core.normalize_whitespace`, etc.) which you can pass directly to `cmp`. |
| 122 | + |
| 123 | +Performance tip: for large lists, precompute normalized keys |
| 124 | + |
| 125 | +When sorting large lists by normalized strings, calling `normalize` on every comparison is expensive. Use the decorate-sort-undecorate pattern: |
| 126 | + |
| 127 | +```gleam |
| 128 | +import gleam/list |
| 129 | +
|
| 130 | +// Precompute normalized keys once |
| 131 | +let decorated = list.map(users, fn(u) { |
| 132 | + let normalized_name = str.extra.ascii_fold(u.name) |
| 133 | + #(u, normalized_name) |
| 134 | +}) |
| 135 | +
|
| 136 | +// Sort by the precomputed key |
| 137 | +let sorted_decorated = list.sort(decorated, by: cmp.by(fn(pair) { pair.1 }, string.compare)) |
| 138 | +
|
| 139 | +// Extract the original values |
| 140 | +let sorted_users = list.map(sorted_decorated, fn(pair) { pair.0 }) |
| 141 | +``` |
| 142 | + |
| 143 | +Further documentation and examples will be published on <https://hexdocs.pm/cmp_gleam>. |
| 144 | + |
| 145 | +## Development |
| 146 | + |
| 147 | +```sh |
| 148 | +gleam run # Run the project |
| 149 | +gleam test # Run the tests |
| 150 | +``` |
0 commit comments