Serapeum Documentation

repository·master·Indexed 19 days ago

https://github.com/ruricolist/serapeum

A conservative Common Lisp utility library designed to supplement Alexandria without causing package conflicts. It provides modular subsystems for binding macros, control flow, type utilities, and sequence operations. Key features include compile-time exhaustiveness checking for pattern matching, block compilation tools (local* and block-compile), and specialized macros for defining iteration and case-like constructs.

Tokens
25.8K
Snippets
110
Records
144
Agent score
18%

What's inside Serapeum

  1. Use Serapeum package-local nicknames

    master

    Serapeum is designed to be conservative and avoid package conflicts. You can use it alongside cl and alexandria by using package-local nicknames. Alternatively, you can use the serapeum/bundle package which re-exports symbols from both serapeum and alexandria (plus other utilities) into a single nickname.

    ;; Standard usage to avoid conflicts
    (defpackage ... (:use #:cl #:alexandria #:serapeum))
    
    ;; Using package-local nicknames for a bundle of utilities
    (defpackage ... (:local-nicknames (:util :serapeum/bundle))))
  2. Divide sequences using Serapeum utilities

    master

    Serapeum provides a family of sequence-related functions with distinctive names to avoid common 'split/divide/group' terminology. These functions are efficient, return like-for-like sequences (e.g., lists for lists, strings for strings), and accommodate generic sequences.

    • runs: Returns runs of like elements in a sequence.
    • batches: Returns a sequence in batches of a certain maximum size.
    • assort: Groups like elements of a sequence based on a :key property.
    • partition: Takes a predicate and returns two sequences: elements for which the predicate is true, and elements for which it is false.
    • partitions: A generalized partition that takes multiple functions and returns items satisfying each condition. Items not satisfying any condition are returned as a second value.
    • split-sequence: Serapeum re-exports this for string splitting.
    ;; runs: returns runs of like elements
    (runs '(head tail head head tail))
    ;; => '((head) (tail) (head head) (tail))
    
    ;; batches: returns sequence in batches of max size
    (batches (iota 11) 2)
    ;; => ((0 1) (2 3) (4 5) (6 7) (8 9) (10))
    
    ;; assort: groups elements by a key
    (assort (iota 10) :key (lambda (n) (mod n 3)))
    ;; => '((0 3 6 9) (1 4 7) (2 5 8))
    
    ;; partition: splits sequence by predicate
    (partition #'oddp (iota 10))
    ;; => (1 3 5 7 9), (0 2 4 6 8)
    
    ;; partitions: splits sequence by multiple predicates
    (partitions (list #'primep #'evenp) (iota 10))
    ;; => ((2 3 5 7) (0 4 6 8)), (1 9)
  3. Use `local` for internal definitions

    master

    The local form allows you to use top-level definition forms (like defun, defmacro, and def) to create local bindings. This is useful for porting code with flat bindings, managing complex nested functions, or using macro-defining macros to create local bindings.

    Serapeum's implementation supports variables, functions, and symbol macros, but has restricted support for macros.

    (local
      (defmemo fibonacci (n)
        (if (<= n 1)
            1
            (+ (fibonacci (- n 1))
               (fibonacci (- n 2)))))
    
      (fibonacci 100))
  4. Use J-style combinators: hook and fork

    master

    Serapeum provides several functional combinators inspired by the J programming language.

    Monadic Hook (hook)

    The hook of f is defined as f(y, g(y)). Also known as Schoenfinkel's S combinator.

    (hook #'= #'floor)
    (funcall * 2.0) ; => T

    Monadic Fork (fork)

    The monadic fork of f, g, and h is defined as (f g h) y <-> (f y) g (h y).

    (fork #'/ #'sum #'length)
    (funcall * '(1.0 2.0 3.0 4.0)) ; => 2.5

    Dyadic Hook (hook2)

    Defined as (hook2 f g) x y <-> f(x, g(x, y)).

    (hook2 #'+ (partial (flip #'/) 60))
    (funcall * 3.0 15.0) ; => 3.25

    Dyadic Fork (fork2)

    Defined as x (f g h) y <-> (x f y) g (x h y).

    (fork2 #'list #'+ #'-)
    (funcall * 10 2) ; => '(12 8)

    Capped Forks (capped-fork, capped-fork2)

    These are versions of the fork where the first function F is omitted, effectively performing composition of G and H.

  5. Optimize sequence access with `with-type-dispatch`

    master

    The with-type-dispatch macro is used to write fast, portable sequence functions by specializing code for different types. It produces one copy of BODY for each type in TYPES, allowing the Lisp to optimize each version.

    Key Features:

    • Transparent Portability: It deduplicates types that are not distinct on the current Lisp implementation.
    • Vector Optimization: Inside the macro, vref is shadowed to expand into the appropriate specialized accessor (e.g., schar for simple-string) based on the specialized type. This is faster than aref on many implementations.
    • Usage Note: VAR should be treated as read-only. This macro is intended for relatively expensive code, such as loops, to justify the dispatch overhead.

    Specialized Variants:

    • with-subtype-dispatch: Similar to with-type-dispatch, but all SUBTYPES must be subtypes of TYPE.
    • with-string-dispatch: A specialized version of with-subtype-dispatch where the overall type is string.
    • with-vector-dispatch: A specialized version of with-subtype-dispatch where the overall type is vector.
    • with-simple-vector-dispatch: On supported implementations, dereferences the underlying simple vector of a displaced array to guarantee the type is a subtype of simple-array.
    (with-type-dispatch (string simple-string) var
      (vref var 0))
  6. Avoid heap allocation in call-with macros using with-thunk

    master

    When writing macros in the call-with- style, you typically wrap the body in a thunk (a lambda). However, these thunks are often allocated on the heap.

    with-thunk provides a way to write these macros without the boilerplate of manually declaring dynamic-extent on a named closure. It allows you to define a thunk that is optimized to avoid heap allocation.

    Usage Patterns:

    1. Basic usage:
    (defmacro with-foo (&body body)
      (with-thunk (body)
        `(call-with-foo ,body)))
    1. With a name for debugging:
    (with-thunk ((body :name foo)) ...)
    1. With arguments:
    (with-thunk (body foo)
      `(call-with-foo ,body))
    ;; Equivalent to:
    ;; (flet ((,body (,foo) ,@body)) (declare (dynamic-extent #',body)) (call-with-foo #',body))
    (with-thunk (spec &rest args) &body body)
  7. Conditionalize compilation with `with-boolean`

    master

    The with-boolean macro establishes a lexical environment for macroexpand-time branching. It allows you to include or exclude code at compilation time based on the value of symbols provided in the first argument.

    Available Branching Macros:

    • (boolean-if branch then &optional else): Includes then if branch is true, otherwise else.
    • (boolean-when branch &body body): Includes body if branch is true.
    • (boolean-unless branch &body body): Includes body if branch is false.

    Note: These macros must be used within the lexical scope of a with-boolean form. The branch argument must be a symbol naming a variable defined in the with-boolean call.

    (with-boolean (flag) 
      (boolean-if flag
        (print "Flag is true")
        (print "Flag is false")))
  8. Create local definitions with `local`

    master

    The local macro allows you to define lexical variables, functions, and symbol macros within a specific scope, similar to Racket's internal definitions. It uses macroexpand-1 to recognize standard definition forms.

    Supported definition forms:

    • def: Lexical variables (like letrec).
    • define-values: Multiple lexical variables.
    • defun: Local functions (translated to defalias).
    • defalias: Bind values in the function namespace.
    • declaim: Local declarations.
    • defconstant / defconst: Symbol macros.
    • define-symbol-macro: Symbol macros.
    • defmacro: Local macros (with restrictions).

    Restrictions for defmacro:

    1. Macros defined with defmacro must precede all other expressions in the local body.
    2. Macros cannot be defined inside binding forms like let.
    3. macrolet is not allowed at the top level of a local form.

    Return Value: The value of the last form in the local body. Note that definitions themselves have return values (e.g., a defun returns the function symbol).

    (local
      (def x 1)
      (def y (1+ x))
      y) ; => 2
    
    (local
      (defun adder (y)
        (+ x y))
      (def x 2)
      (adder 1)) ; => 3
  9. Load specific Serapeum modules

    master

    Serapeum is refactored into modular subsystems. While loading the whole system is recommended, you can load specific modules using ql:quickload to incrementally adopt features or reduce dependencies.

    (ql:quickload "serapeum/types")
  10. Switch to block compilation with `local*` and `block-compile`

    master

    Serapeum provides macros to facilitate block compilation of top-level functions, which is useful for performance in Lisps that lack native syntax for it.

    • local*: Similar to local, but leaves the last form in the body intact. This is useful for converting a progn block of top-level definitions into a block-compiled structure. Note that calls to entry points (including self-calls) may still be compiled as global calls.
    • block-compile: A more robust option that allows you to specify :entry-points. This ensures that calls to the entry points are compiled as local calls.

    To use block-compile, provide the entry point function names in the :entry-points keyword argument.

    ;; Using local*
    (local*
      (defun aux-fn-1 ...)
      (defun aux-fn-2 ...)
      (defun entry-point ...))
    
    ;; Using block-compile for local calls
    (block-compile (:entry-points (entry-point))
      (defun aux-fn-1 ...)
      (defun aux-fn-2 ...)
      (defun entry-point ...))
  11. Manage queues with the Queue API

    master

    Serapeum provides Norvig-style queues wrapped in objects to prevent printer overflow, featuring a concise API inspired by Arc. Queues support standard FIFO operations as well as prepending and concatenation.

    ;; Creation and Inspection
    (queue &rest initial-contents) ; Build a new queue
    (queuep g)                     ; Test if g is a queue
    (qlen queue)                   ; Number of items
    (queue-empty-p queue)         ; Is it empty?
    (qlist queue)                  ; Return items as a list (does not cons)
    
    ;; Basic FIFO Operations
    (enq item queue)              ; Insert ITEM at the end
    (deq queue)                    ; Remove item from the front
    (front queue)                  ; Get first element
    (qback queue)                  ; Get last element
    
    ;; Prepending and Undoing
    (undeq item queue)             ; Add ITEM to the front (can undo a deq)
    (qprepend list queue)          ; Insert LIST at the beginning
    
    ;; Concatenation and Copying
    (qappend queue list)           ; Append LIST to the end
    (qconc queue list)             ; Destructively concatenate LIST onto the end
    (qpreconc list queue)           ; Destructively splice LIST at the beginning
    (copy-queue queue)             ; Create a copy of the queue
    
    ;; Resetting
    (clear-queue queue)             ; Return contents and reset queue