sema-medley
0.1.0Utility belt: map/list transformations the stdlib doesn't cover
sema-medley
Utility belt: map/list transformations the stdlib doesn't cover.
Modeled on Clojure's medley, Common Lisp's alexandria, and Emacs's dash.el — but deliberately small. The Sema stdlib is already broad; sema-medley only adds what's genuinely missing.
All functions are pure: they return new values and never mutate their
arguments. Map functions operate on sorted maps (map?); convert a hashmap
first with hashmap/to-map. List arguments accept nil as the empty list.
Install
sema pkg add sema-medley
Quick start
(import "sema-medley")
;; Build an API payload from optional inputs — nil values are skipped:
(m/assoc-some {:name "Ada"} :email nil :role "admin")
; => {:name "Ada" :role "admin"}
;; Pick the record with the largest key:
(m/max-by :age (list {:name "Ada" :age 36} {:name "Bob" :age 25}))
; => {:age 36 :name "Ada"}
Design notes
The stdlib already covers most of the classic medley/alexandria surface, so
none of it is duplicated here: map/deep-merge, map/map-vals,
map/map-keys, map/select-keys, map/except, map/get-in,
map/assoc-in, map/update-in, list/group-by, list/key-by (index-by),
list/find (find-first), list/unique (distinct), list/chunk
(partition-all), frequencies, list/take-while, list/drop-while, zip,
flatten, and list/interleave all ship with Sema. sema-medley adds the
inverses, the by-key variants, and the positional edits those leave out.
Sema's sorted maps make every "last wins" rule here deterministic — see
m/map-invert.
API
Maps
| Function | Description |
|---|---|
(m/map-invert m) |
Swap keys and values: (m/map-invert {:a 1}) → {1 :a} |
(m/filter-keys pred m) |
Keep entries whose key matches: (m/filter-keys #(not (equal? % :b)) {:a 1 :b 2}) → {:a 1} |
(m/filter-vals pred m) |
Keep entries whose value matches: (m/filter-vals even? {:a 1 :b 2}) → {:b 2} |
(m/remove-vals pred m) |
Drop entries whose value matches: (m/remove-vals nil? {:a 1 :b nil}) → {:a 1} |
(m/assoc-some m k v ...) |
Assoc only non-nil values: (m/assoc-some {} :a 1 :b nil) → {:a 1} |
(m/update-existing m k f) |
Update only if k is present: (m/update-existing {:a 1} :z #(+ % 1)) → {:a 1} |
(m/dissoc-in m path) |
Remove a nested key: (m/dissoc-in {:a {:b 1 :c 2}} [:a :b]) → {:a {:c 2}} |
Lists
| Function | Description |
|---|---|
(m/find-index pred xs) |
Index of first match: (m/find-index even? '(1 3 4)) → 2 |
(m/distinct-by f xs) |
Dedupe by key, first wins: (m/distinct-by string/lower '("A" "a" "b")) → ("A" "b") |
(m/partition-by f xs) |
Split into runs of equal (f x): (m/partition-by even? '(1 3 2 4 5)) → ((1 3) (2 4) (5)) |
(m/take-upto pred xs) |
Take through first match, inclusive: (m/take-upto even? '(1 3 4 5)) → (1 3 4) |
(m/drop-upto pred xs) |
Drop through first match, inclusive: (m/drop-upto even? '(1 3 4 5)) → (5) |
(m/insert-nth n x xs) |
Insert at index: (m/insert-nth 1 :x '(1 2)) → (1 :x 2) |
(m/remove-nth n xs) |
Remove at index: (m/remove-nth 1 '(1 2 3)) → (1 3) |
(m/replace-nth n x xs) |
Replace at index: (m/replace-nth 1 :x '(1 2 3)) → (1 :x 3) |
(m/interleave-all xs ys) |
Interleave, keeping leftovers: (m/interleave-all '(1 2 3) '("a")) → (1 "a" 2 3) |
(m/min-by f xs) |
Element with smallest key: (m/min-by abs '(-5 2 -1)) → -1 |
(m/max-by f xs) |
Element with largest key: (m/max-by string-length '("a" "bb")) → "bb" |
Anywhere a function argument is expected, a keyword also works — keywords are
callable in Sema, so (m/max-by :age people) reads naturally.
m/map-invert
(m/map-invert {:a 1 :b 2})
; => {1 :a 2 :b}
Swap the keys and values of a map. When two keys share a value, the entry whose key sorts last wins, because map key iteration is sorted:
(m/map-invert {:a 1 :b 1})
; => {1 :b}
m/filter-keys
(m/filter-keys (fn (k) (not (equal? k :password)))
{:user "ada" :password "hunter2"})
; => {:user "ada"}
Keep only the entries whose key satisfies pred.
m/filter-vals
(m/filter-vals even? {:a 1 :b 2 :c 3 :d 4})
; => {:b 2 :d 4}
Keep only the entries whose value satisfies pred.
m/remove-vals
(m/remove-vals nil? {:name "Ada" :email nil})
; => {:name "Ada"}
Drop the entries whose value satisfies pred — the inverse of
m/filter-vals. (m/remove-vals nil? m) is the classic "strip nils before
JSON-encoding" move.
m/assoc-some
(m/assoc-some {:name "Ada"} :email nil :role "admin" :team nil)
; => {:name "Ada" :role "admin"}
Assoc each key/value pair whose value is not nil; nil values are skipped
entirely, so existing entries are never erased by them. #f is a real value
and is kept. Errors on an odd number of key/value arguments.
m/update-existing
(m/update-existing {:count 1} :count #(+ % 1)) ; => {:count 2}
(m/update-existing {:count 1} :views #(+ % 1)) ; => {:count 1}
Apply f to the value at k only if the map contains k; otherwise return
the map unchanged. A key present with value nil counts as present.
m/dissoc-in
(m/dissoc-in {:user {:name "Ada" :token "s3cret"}} [:user :token])
; => {:user {:name "Ada"}}
Remove the key at a nested path (vector or list of keys) — the removal
counterpart to map/assoc-in. If any key along the path is missing or leads
to a non-map, the map is returned unchanged. Intermediate maps left empty by
the removal are kept, not pruned. Errors on an empty path.
m/find-index
(m/find-index even? '(1 3 4 6)) ; => 2
(m/find-index even? '(1 3 5)) ; => nil
Return the zero-based index of the first element satisfying pred, or nil
if none does — the predicate counterpart to list/index-of.
m/distinct-by
(m/distinct-by :id (list {:id 1 :v "x"} {:id 2 :v "y"} {:id 1 :v "z"}))
; => ({:id 1 :v "x"} {:id 2 :v "y"})
Remove elements whose key (f x) has already been seen, keeping the first
occurrence and preserving order — list/unique with a key function. Keys may
be any value, including nil.
m/partition-by
(m/partition-by even? '(1 3 2 4 5))
; => ((1 3) (2 4) (5))
Split a list into runs of consecutive elements on which f returns equal
values. Every element lands in exactly one run, in the original order —
(apply append (m/partition-by f xs)) is always xs. Distinct from
partition (predicate split into two lists) and list/chunk (fixed sizes).
m/take-upto
(m/take-upto even? '(1 3 4 5 6)) ; => (1 3 4)
(m/take-upto even? '(1 3 5)) ; => (1 3 5)
Take elements up to and including the first one satisfying pred — the
inclusive variant of list/take-while. Note the predicate marks the sentinel
element, not what to keep. If nothing matches, the whole list is returned.
m/drop-upto
(m/drop-upto even? '(1 3 4 5 6)) ; => (5 6)
(m/drop-upto even? '(1 3 5)) ; => ()
Drop elements up to and including the first one satisfying pred. For
any list, (append (m/take-upto p xs) (m/drop-upto p xs)) is xs.
m/insert-nth
(m/insert-nth 1 :x '(1 2 3)) ; => (1 :x 2 3)
(m/insert-nth 2 :x '(1 2)) ; => (1 2 :x)
Insert x at index n, shifting later elements right. n may equal the
length (append at the end); anything outside 0..length errors.
m/remove-nth
(m/remove-nth 1 '(1 2 3)) ; => (1 3)
Remove the element at index n. Errors when n is outside 0..length-1.
m/replace-nth
(m/replace-nth 1 :x '(1 2 3)) ; => (1 :x 3)
Replace the element at index n with x. Errors when n is outside
0..length-1.
m/interleave-all
(m/interleave-all '(1 2 3) '("a")) ; => (1 "a" 2 3)
Interleave two lists, appending the leftover tail of the longer one — unlike
list/interleave, which stops at the shorter list. No elements are dropped.
m/min-by
(m/min-by :age (list {:name "Ada" :age 36} {:name "Bob" :age 25}))
; => {:age 25 :name "Bob"}
Return the element with the smallest key (f x), or nil for an empty list.
Keys are compared with < and must be mutually comparable (all numbers or
all strings). On ties the first element wins. The element-returning
counterpart to list/min.
m/max-by
(m/max-by string-length '("a" "ccc" "bb"))
; => "ccc"
Return the element with the largest key (f x), or nil for an empty list.
Same comparison and tie rules as m/min-by.
Testing
sema pkg add sema-test # once
sema tests.sema
License
MIT
| Version | Size | Published |
|---|---|---|
| 0.1.0 | 9 KB | 2026-07-07 21:28:09 |