sema-medley

0.1.0

Utility belt: map/list transformations the stdlib doesn't cover

$ sema pkg install sema-medley
Readme Versions 1 Dependencies 0

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

VersionSizePublished
0.1.0 9 KB 2026-07-07 21:28:09
No dependencies